export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({totalMass:.18,hangingMass:.015,resistance:.005,gravity:9.8,gateSpacing:.5,initialSpeed:.2,flagLength:.002});export const PARAMETER_BOUNDS=Object.freeze({totalMass:[.08,.5],hangingMass:[.005,.025],resistance:[0,.04],gravity:[9.8,9.8],gateSpacing:[.2,.6],initialSpeed:[.2,.5],flagLength:[.001,.005]}); 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 dynamics(p){validateParameters(p);const force=p.hangingMass*p.gravity,acceleration=(force-p.resistance)/p.totalMass,finalSpeed=Math.sqrt(p.initialSpeed**2+2*acceleration*p.gateSpacing),travelTime=2*p.gateSpacing/(p.initialSpeed+finalSpeed),tension=(p.totalMass-p.hangingMass)*acceleration+p.resistance;return{force,acceleration,initialSpeed:p.initialSpeed,finalSpeed,travelTime,tension};} export function motion(p,t){const q=dynamics(p);if(!Number.isFinite(t)||t<0)throw new RangeError('invalid time');return{position:p.initialSpeed*t+.5*q.acceleration*t*t,velocity:p.initialSpeed+q.acceleration*t};} export function gateDuration(speed,acceleration,width){if(![speed,acceleration,width].every(Number.isFinite)||speed<=0||acceleration<0||width<=0||speed*speed<=acceleration*width)throw new RangeError('invalid finite-width gate interval');return 2*width/(Math.sqrt(speed*speed+acceleration*width)+Math.sqrt(speed*speed-acceleration*width));} export function gateReadings(p){const q=dynamics(p),time1=gateDuration(q.initialSpeed,q.acceleration,p.flagLength),time2=gateDuration(q.finalSpeed,q.acceleration,p.flagLength);return{time1,time2,...fromGate(p,time1,time2)};} export function fromGate(p,time1,time2){validateParameters(p);if(![time1,time2].every(Number.isFinite)||time1<=0||time2<=0)throw new RangeError('过门时间须为正有限数');const speed1=p.flagLength/time1,speed2=p.flagLength/time2,acceleration=(speed2*speed2-speed1*speed1)/(2*p.gateSpacing);return{speed1,speed2,acceleration};} export function lineFit(x,y){if(x.length!==y.length||x.length<3||![...x,...y].every(Number.isFinite))throw new RangeError('至少记录3组有限值');const mx=x.reduce((a,b)=>a+b)/x.length,my=y.reduce((a,b)=>a+b)/y.length,sxx=x.reduce((s,v)=>s+(v-mx)**2,0);if(sxx<1e-20)throw new RangeError('须包含不同加速度');const slope=x.reduce((s,v,i)=>s+(v-mx)*(y[i]-my),0)/sxx,intercept=my-slope*mx,residuals=y.map((v,i)=>v-slope*x[i]-intercept),slopeSE=Math.sqrt(residuals.reduce((s,v)=>s+v*v,0)/(x.length-2)/sxx);return{slope,intercept,slopeSE,residuals};} export function sweep(p,source='ideal-dynamics'){validateParameters(p);if(!['ideal-dynamics','ideal-gates'].includes(source))throw new RangeError('unknown source');return[.005,.010,.015,.020,.025].map(hangingMass=>{const parameters={...p,hangingMass},q=dynamics(parameters),gate=gateReadings(parameters);return{experiment:'course-01/newton-second-law',modelVersion:MODEL_VERSION,source,parameters,raw:source==='ideal-gates'?{time1:gate.time1,time2:gate.time2}:{initialSpeed:q.initialSpeed,finalSpeed:q.finalSpeed},acceleration:source==='ideal-gates'?gate.acceleration:q.acceleration};});} export function fitRecords(rows,source){const same=rows.filter(r=>r.source===source);if(same.length<3)throw new RangeError('至少记录3组相同来源数据');const p=same[0].parameters;for(const r of same)for(const k of Object.keys(PARAMETER_BOUNDS).filter(k=>k!=='hangingMass'))if(r.parameters[k]!==p[k])throw new RangeError('总质量、阻力、门距、初速、挡光片须保持不变');const fit=lineFit(same.map(r=>r.acceleration),same.map(r=>r.parameters.hangingMass));return{...fit,inferredMass:fit.slope*p.gravity,equivalentResistance:fit.intercept*p.gravity};}