export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({conductivity:.2,specificHeat:1500,density:1124,thickness:.01,voltage:18.02,resistance:110,area:.85*.09*.09,thermocouple:40,initialTemperature:20});export const PARAMETER_BOUNDS=Object.freeze({conductivity:[.1,.4],specificHeat:[1000,2000],density:[1124,1124],thickness:[.005,.015],voltage:[10,20],resistance:[110,110],area:[.85*.09*.09,.85*.09*.09],thermocouple:[40,40],initialTemperature:[20,20]}); 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 properties(p){validateParameters(p);const heatFlux=p.voltage**2/(2*p.area*p.resistance),diffusivity=p.conductivity/(p.density*p.specificHeat),meanRate=heatFlux/(p.density*p.specificHeat*p.thickness),quasiDifference=heatFlux*p.thickness/(2*p.conductivity);return{heatFlux,diffusivity,meanRate,quasiDifference};} function frame(p,T,t,dx){const q=properties(p),N=T.length,center=t===0?p.initialTemperature:(9*T[0]-T[1])/8,heater=t===0?p.initialTemperature:T[N-1]+q.heatFlux*dx/(2*p.conductivity)-(q.heatFlux/p.conductivity-(T[N-1]-T[N-2])/dx)*dx/8,mean=T.reduce((s,v)=>s+v,0)/N;return{time:t,temperatures:[...T],positions:T.map((_,i)=>(i+.5)*dx),center,heater,mean,meanExact:p.initialTemperature+q.meanRate*t,centerVoltage:(center-p.initialTemperature)*p.thermocouple,heaterVoltage:(heater-p.initialTemperature)*p.thermocouple};} export function simulate(p,{cells=40,duration=1800,outputStep=60}={}){const q=properties(p);if(!Number.isInteger(cells)||cells<10||cells>100||!Number.isFinite(duration)||duration<0||duration>1800||!Number.isFinite(outputStep)||outputStep<=0)throw new RangeError('invalid mesh/time');const dx=p.thickness/cells,dtMax=Math.min(1,.4*dx*dx/q.diffusivity),source=q.heatFlux/(p.density*p.specificHeat*dx);let T=Array(cells).fill(p.initialTemperature),t=0;const frames=[frame(p,T,0,dx)];for(let target=Math.min(outputStep,duration);target>0;target=Math.min(target+outputStep,duration)){while(tv+.5*r*((i>0?T[i-1]-v:0)+(i0?-.5*r:0,upper=i=0;i--)next[i]=dPrime[i]-cPrime[i]*next[i+1];T=next;t+=dt;}t=target;frames.push(frame(p,T,t,dx));if(target===duration)break;}return{parameters:{...p},cells,method:'cell-centered finite volume + Crank–Nicolson, positivity ratio<=0.4',dtMax,frames};} export function inferConductivity(p,centerVoltage,heaterVoltage){const q=properties(p);if(![centerVoltage,heaterVoltage].every(Number.isFinite)||heaterVoltage<=centerVoltage)throw new RangeError('加热面电压须大于中心面');const difference=(heaterVoltage-centerVoltage)/p.thermocouple;return{conductivity:q.heatFlux*p.thickness/(2*difference),difference};} 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,centerVoltage,heaterVoltage,source,cells=40){validateParameters(p);if(!Number.isFinite(time)||time<0||time>1800||!['ideal-model','manual-voltage'].includes(source))throw new RangeError('invalid reading');const derived=inferConductivity(p,centerVoltage,heaterVoltage);return{experiment:'course-02/thermal-conductivity',modelVersion:MODEL_VERSION,source,cells,parameters:{...p},raw:{time,centerVoltage,heaterVoltage},derived};} export function fitSpecificHeat(rows,source,minimumTime=780){const a=rows.filter(r=>r.source===source&&r.raw.time>=minimumTime);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('材料、几何和加热条件须保持一致');if(r.cells!==a[0].cells)throw new RangeError('不同网格记录单独分析');}const fit=lineFit(a.map(r=>r.raw.time),a.map(r=>r.raw.centerVoltage/p.thermocouple));if(fit.slope<=0)throw new RangeError('中心温升斜率须为正');return{...fit,specificHeat:properties(p).heatFlux/(p.density*p.thickness*fit.slope),count:a.length};}