export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({platformInertia:.0056,ringMass:.436,ringOuter:.12,ringInner:.105,cylinderMass:.166,cylinderRadius:.015,offset:.1,hangingMass:.05,towerRadius:.025,frictionTorque:.0005,initialOmega:5,gravity:9.8});export const PARAMETER_BOUNDS=Object.freeze({platformInertia:[.003,.008],ringMass:[.436,.436],ringOuter:[.12,.12],ringInner:[.105,.105],cylinderMass:[.166,.166],cylinderRadius:[.015,.015],offset:[0,.15],hangingMass:[.02,.1],towerRadius:[.015,.03],frictionTorque:[.0002,.001],initialOmega:[5,10],gravity:[9.8,9.8]}); export function validateParameters(p){for(const[k,[lo,hi]]of Object.entries(PARAMETER_BOUNDS))if(!Number.isFinite(p[k])||p[k]hi)throw new RangeError(`${k} outside [${lo},${hi}]`);return p;} export function inertia(p,mount='platform'){validateParameters(p);if(!['platform','ring','cylinders'].includes(mount))throw new RangeError('unknown mounting');const ring=.5*p.ringMass*(p.ringOuter**2+p.ringInner**2),singleCylinder=.5*p.cylinderMass*p.cylinderRadius**2+p.cylinderMass*p.offset**2;return{total:p.platformInertia+(mount==='ring'?ring:mount==='cylinders'?2*singleCylinder:0),ring,singleCylinder};} export function accelerations(p,mount='platform'){const J=inertia(p,mount).total;return{free:-p.frictionTorque/J,driven:(p.hangingMass*p.gravity*p.towerRadius-p.frictionTorque)/(J+p.hangingMass*p.towerRadius**2)};} export function gateTimes(p,mount='platform',stage='driven'){if(!['free','driven'].includes(stage))throw new RangeError('unknown stage');const beta=accelerations(p,mount)[stage];return Array.from({length:8},(_,i)=>{const theta=(i+1)*Math.PI,disc=p.initialOmega**2+2*beta*theta;if(disc<=0)throw new RangeError('自由减速未达到第8遮光信号,请提高初速');return 2*theta/(p.initialOmega+Math.sqrt(disc));});} export function motion(p,t,mount='platform',stage='driven'){const beta=accelerations(p,mount)[stage];if(!Number.isFinite(t)||t<0||p.initialOmega+beta*t<0)throw new RangeError('constant-friction model requires forward rotation');return{angle:p.initialOmega*t+.5*beta*t*t,omega:p.initialOmega+beta*t,beta};} export function estimateBeta(times){if(!Array.isArray(times)||times.length!==8||times.some((v,i)=>!Number.isFinite(v)||v<=0||(i>0&&v<=times[i-1])))throw new RangeError('八个遮光时间须为严格递增正数');const pairs=Array.from({length:4},(_,i)=>{const m=i+1,n=i+5,tm=times[i],tn=times[i+4];return 2*Math.PI*(n*tm-m*tn)/(tm*tn*tn-tm*tm*tn);});return{pairs,beta:pairs.reduce((s,v)=>s+v,0)/4};} export function inferInertia(p,betaFree,betaDriven){validateParameters(p);if(!Number.isFinite(betaFree)||!Number.isFinite(betaDriven)||betaFree>=0||betaDriven<=0)throw new RangeError('减速β须为负、加速β须为正');return p.hangingMass*p.towerRadius*(p.gravity-p.towerRadius*betaDriven)/(betaDriven-betaFree);} export function measurement(p,mount,stage,times,source){inertia(p,mount);if(!['free','driven'].includes(stage)||!['ideal-model','manual-timing'].includes(source))throw new RangeError('unknown mode/source');return{experiment:'course-02/moment-of-inertia',modelVersion:MODEL_VERSION,mount,stage,source,parameters:{...p},raw:{times:[...times],angles:times.map((_,i)=>(i+1)*Math.PI)},derived:estimateBeta(times)};} export function analyzeRecords(rows,p,source){const same=rows.filter(r=>r.source===source&&Object.keys(PARAMETER_BOUNDS).every(k=>r.parameters[k]===p[k])),result={};for(const mount of['platform','ring','cylinders']){const selected=same.filter(r=>r.mount===mount),free=selected.filter(r=>r.stage==='free').at(-1),driven=selected.filter(r=>r.stage==='driven').at(-1);if(free&&driven)result[mount]=inferInertia(p,free.derived.beta,driven.derived.beta);}if(result.platform!==undefined&&result.ring!==undefined)result.ringSample=result.ring-result.platform;if(result.platform!==undefined&&result.cylinders!==undefined)result.singleCylinder=(result.cylinders-result.platform)/2;return result;}