export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({surfaceTension:.0728,innerDiameter:.033008,outerDiameter:.034944,contactAngle:0,filmMass:0,conversion:.00036,baseline:85.6,gravity:9.794});export const PARAMETER_BOUNDS=Object.freeze({surfaceTension:[.02,.08],innerDiameter:[.02,.045],outerDiameter:[.022,.05],contactAngle:[0,60],filmMass:[0,.0002],conversion:[.0002,.0006],baseline:[50,120],gravity:[9.794,9.794]}); 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}]`);if(p.outerDiameter<=p.innerDiameter)throw new RangeError('外径须大于内径');return p;} export function properties(p){validateParameters(p);const contactLength=Math.PI*(p.innerDiameter+p.outerDiameter),surfaceForce=p.surfaceTension*contactLength*Math.cos(p.contactAngle*Math.PI/180),filmWeight=p.filmMass*p.gravity,force=surfaceForce+filmWeight,after=p.baseline,before=after+force/p.conversion;return{contactLength,surfaceForce,filmWeight,force,before,after,difference:before-after};} export function calibrationVoltage(p,mass){validateParameters(p);if(!Number.isFinite(mass)||mass<0||mass>.0035)throw new RangeError('标定质量须在0–3.5g');return p.baseline+mass*p.gravity/p.conversion;} 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);return{slope,intercept,residuals};} export function inferSurface(p,before,after,conversion){validateParameters(p);if(![before,after,conversion].every(Number.isFinite)||before<=after||conversion<=0)throw new RangeError('拉脱前须大于后读数,转换系数须为正');return conversion*(before-after)/properties(p).contactLength;} export function calibrate(rows,p,source){const a=rows.filter(r=>r.source===source);for(const r of a)for(const k of['conversion','baseline','gravity'])if(r.parameters[k]!==p[k])throw new RangeError('标定的传感器参数须一致');const fit=lineFit(a.map(r=>r.voltage),a.map(r=>r.mass*p.gravity));if(fit.slope<=0)throw new RangeError('标定转换系数须为正');return fit;} export function calibrationRecord(p,mass,voltage,source){calibrationVoltage(p,mass);if(!Number.isFinite(voltage)||voltage<0||voltage>500||!['ideal-model','manual-voltage'].includes(source))throw new RangeError('标定电压须为0–500mV');return{experiment:'course-02/surface-tension',modelVersion:MODEL_VERSION,type:'calibration',source,parameters:{...p},mass,voltage};} export function pullRecord(p,before,after,source){inferSurface(p,before,after,p.conversion);if(!['ideal-model','manual-voltage'].includes(source))throw new RangeError('unknown source');return{experiment:'course-02/surface-tension',modelVersion:MODEL_VERSION,type:'pull',source,parameters:{...p},before,after};} export function calibrationSweep(p){return Array.from({length:8},(_,i)=>{const mass=i*.0005;return calibrationRecord(p,mass,calibrationVoltage(p,mass),'ideal-model');});}