export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({youngModulus:2e11,wireLength:.720,wireDiameter:.000520,mirrorDistance:.690,leverArm:.03526,mass:5,gravity:9.8,scaleOrigin:.032,trueZero:.000062,assumedZero:.000062});export const PARAMETER_BOUNDS=Object.freeze({youngModulus:[1e11,2.5e11],wireLength:[.5,1.0],wireDiameter:[.0004,.0008],mirrorDistance:[.5,1.5],leverArm:[.03,.05],mass:[0,10],gravity:[9.8,9.8],scaleOrigin:[.032,.032],trueZero:[.000062,.000062],assumedZero:[.00004,.00008]}); 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}]`);const strain=p.mass*p.gravity/(p.youngModulus*Math.PI*p.wireDiameter**2/4),extension=strain*p.wireLength;if(strain>.003||extension/p.leverArm>.1)throw new RangeError('教学线弹性域需应变≤0.003,且光杠杆伸长/臂长≤0.1;请减载或加粗金属丝。');return p;} export function properties(p){validateParameters(p);const area=Math.PI*p.wireDiameter**2/4,force=p.mass*p.gravity,stress=force/area,strain=stress/p.youngModulus,extension=strain*p.wireLength,angle=Math.asin(extension/p.leverArm),scaleShift=p.mirrorDistance*Math.tan(2*angle),linearShift=2*p.mirrorDistance*extension/p.leverArm;return{area,force,stress,strain,extension,angle,scaleShift,linearShift,scaleReading:p.scaleOrigin+scaleShift,apparentDiameter:p.wireDiameter+p.trueZero,correctedDiameter:p.wireDiameter+p.trueZero-p.assumedZero};} export function inferYoung(p,scaleShift,method='linear'){validateParameters(p);if(!Number.isFinite(scaleShift)||scaleShift<=0||p.mass<=0)throw new RangeError('正载荷与正标尺位移才可反演');const q=properties(p);if(!['linear','geometric'].includes(method))throw new RangeError('unknown lever inversion');const extension=method==='linear'?p.leverArm*scaleShift/(2*p.mirrorDistance):p.leverArm*Math.sin(.5*Math.atan(scaleShift/p.mirrorDistance));return p.mass*p.gravity*p.wireLength/(Math.PI*q.correctedDiameter**2/4*extension);} 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((a,v)=>a+(v-mx)**2,0);if(sxx<1e-20)throw new RangeError('须有不同载荷');const slope=x.reduce((a,v,i)=>a+(v-mx)*(y[i]-my),0)/sxx,intercept=my-slope*mx,residuals=y.map((v,i)=>v-slope*x[i]-intercept);return{slope,intercept,residuals};} export function measurement(p,reading,source){validateParameters(p);if(!Number.isFinite(reading)||reading.5)throw new RangeError('标尺读数须在32–500 mm');if(!['ideal-model','manual-scale'].includes(source))throw new RangeError('unknown source');return{experiment:'course-02/young-modulus',modelVersion:MODEL_VERSION,source,parameters:{...p},raw:{scaleReading:reading,apparentDiameter:properties(p).apparentDiameter},derived:{scaleShift:reading-p.scaleOrigin}};} export function sweep(p){return[0,2,4,6,8,10].map(mass=>{const next={...p,mass};return measurement(next,properties(next).scaleReading,'ideal-model');});} export function fitRecords(rows,source){const a=rows.filter(r=>r.source===source);if(a.length<3)throw new RangeError('至少3组相同来源记录');const p=a[0].parameters;for(const r of a)for(const k of Object.keys(PARAMETER_BOUNDS).filter(k=>k!=='mass'))if(r.parameters[k]!==p[k])throw new RangeError('几何、零差和材料参数须保持不变');const fit=lineFit(a.map(r=>r.parameters.mass),a.map(r=>r.raw.scaleReading));if(fit.slope<=0)throw new RangeError('拟合斜率须为正');return{...fit,youngModulus:8*p.gravity*p.wireLength*p.mirrorDistance/(Math.PI*properties(p).correctedDiameter**2*p.leverArm*fit.slope)};}