export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({equivalent:4.1868,heatCapacityCal:125,voltage:20,current:.495,heatTransfer:.05,ambientTemperature:293.15,initialTemperature:291.59});export const PARAMETER_BOUNDS=Object.freeze({equivalent:[4.1868,4.1868],heatCapacityCal:[100,300],voltage:[5,20],current:[.1,.5],heatTransfer:[0,.1],ambientTemperature:[285,305],initialTemperature:[285,305]}); 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 state(p,t,heating=true){validateParameters(p);if(!Number.isFinite(t)||t<0||t>1800)throw new RangeError('时间须在0–1800s');const power=heating?p.voltage*p.current:0,C=p.equivalent*p.heatCapacityCal,H=p.heatTransfer;let temperature;if(H===0)temperature=p.initialTemperature+power*t/C;else temperature=p.initialTemperature+(power-H*(p.initialTemperature-p.ambientTemperature))/H*(-Math.expm1(-H*t/C));const electricalWork=power*t,absorbed=C*(temperature-p.initialTemperature),heatExchange=electricalWork-absorbed;return{temperature,power,electricalWork,absorbed,heatExchange,slope:(power-H*(temperature-p.ambientTemperature))/C};} 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 measurement(p,time,temperature,source,heating=true){validateParameters(p);if(!Number.isFinite(time)||time<0||time>1800||!Number.isFinite(temperature)||temperature<270||temperature>370||!['ideal-model','manual-temperature'].includes(source))throw new RangeError('时间0–1800s、温度270–370K');return{experiment:'course-01/mechanical-equivalent-heat',modelVersion:MODEL_VERSION,source,phase:heating?'heating':'power-off',parameters:{...p},raw:{time,temperature},derived:state(p,time,heating)};} export function sweep(p,heating=true){return Array.from({length:30},(_,i)=>{const time=i*60;return measurement(p,time,state(p,time,heating).temperature,'ideal-model',heating);});} export function fitRecords(rows,source,window='all'){let a=rows.filter(r=>r.source===source&&r.phase==='heating');if(window==='early')a=a.filter(r=>r.raw.time<=14*60);else if(window==='late')a=a.filter(r=>r.raw.time>=15*60);else if(window!=='all')throw new RangeError('unknown window');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))if(r.parameters[k]!==p[k])throw new RangeError('电功、热容、初温和散热条件须保持一致');const fit=lineFit(a.map(r=>r.raw.time),a.map(r=>r.raw.temperature));if(fit.slope<=0)throw new RangeError('升温斜率须为正');return{...fit,equivalentUncorrected:p.voltage*p.current/(p.heatCapacityCal*fit.slope),count:a.length};} export function endpointEquivalent(p,t,temperature){validateParameters(p);if(!Number.isFinite(t)||t<=0||!Number.isFinite(temperature)||temperature<=p.initialTemperature)throw new RangeError('需要正时间和正温升');const heatCal=p.heatCapacityCal*(temperature-p.initialTemperature),W=p.voltage*p.current*t,Qloss=state(p,t).heatExchange;return{uncorrected:W/heatCal,corrected:(W-Qloss)/heatCal};}