export const MODEL_VERSION='1.0.0';export const DEFAULT_PARAMETERS=Object.freeze({frequency:36015,soundSpeed:347.21,position:.1,roundTrip:.7,electronicPhase:0,positionScale:1,positionOffset:0,temperature:26.5});export const PARAMETER_BOUNDS=Object.freeze({frequency:[34000,40000],soundSpeed:[330,360],position:[.05,.15],roundTrip:[.3,.9],electronicPhase:[0,2*Math.PI],positionScale:[.98,1.02],positionOffset:[-.0005,.0005],temperature:[26.5,26.5]}); 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 transfer(p,mode='standing'){validateParameters(p);if(!['standing','phase'].includes(mode))throw new RangeError('unknown mode');const wavelength=p.soundSpeed/p.frequency,k=2*Math.PI/wavelength,phase=k*p.position;let real,imag;if(mode==='phase'){real=Math.cos(-phase);imag=Math.sin(-phase);}else{const dr=1-p.roundTrip*Math.cos(2*phase),di=p.roundTrip*Math.sin(2*phase),den=dr*dr+di*di,nr=(1-p.roundTrip)*Math.cos(phase),ni=-(1-p.roundTrip)*Math.sin(phase);real=(nr*dr+ni*di)/den;imag=(ni*dr-nr*di)/den;}const cr=Math.cos(p.electronicPhase),ci=Math.sin(p.electronicPhase),a=real*cr-imag*ci,b=real*ci+imag*cr;return{real:a,imag:b,amplitude:Math.hypot(a,b),phase:Math.atan2(b,a),wavelength,positionReading:p.position*p.positionScale+p.positionOffset};} export function featurePosition(p,mode,index){validateParameters(p);if(!Number.isInteger(index)||index<0||index>100)throw new RangeError('标序须为0–100整数');return mode==='standing'?index*p.soundSpeed/(2*p.frequency):(index+p.electronicPhase/(2*Math.PI))*p.soundSpeed/p.frequency;} export function nextFeature(p,mode){const period=p.soundSpeed/p.frequency*(mode==='standing'?.5:1),offset=mode==='phase'?p.electronicPhase/(2*Math.PI)*p.soundSpeed/p.frequency:0,index=Math.floor((p.position-offset)/period+1e-9)+1,position=featurePosition(p,mode,index);if(position>.15)throw new RangeError('已到量程末端,请把位置调回较小值');return{index,position};} export function measurement(p,mode,index,positionReading,source){validateParameters(p);featurePosition(p,mode,index);if(!Number.isFinite(positionReading)||positionReading<.04||positionReading>.16||!['ideal-model','manual-position'].includes(source))throw new RangeError('位置读数须在40–160mm');return{experiment:'course-02/sound-speed',modelVersion:MODEL_VERSION,source,mode,parameters:{...p},raw:{index,positionReading},derived:{phase:transfer(p,mode).phase,amplitude:transfer(p,mode).amplitude}};} 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 fitRecords(rows,mode,source){const a=rows.filter(r=>r.mode===mode&&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!=='position'))if(r.parameters[k]!==p[k])throw new RangeError('频率、介质和标尺条件须保持一致');const fit=lineFit(a.map(r=>r.raw.index),a.map(r=>r.raw.positionReading));if(fit.slope<=0)throw new RangeError('位置斜率须为正');const wavelength=fit.slope*(mode==='standing'?2:1),speed=wavelength*p.frequency,gamma=.02896*speed*speed/(8.31*(p.temperature+273.15));return{...fit,wavelength,speed,gamma};} export function sweep(p,mode){validateParameters(p);const period=p.soundSpeed/p.frequency*(mode==='standing'?.5:1),offset=mode==='phase'?p.electronicPhase/(2*Math.PI)*p.soundSpeed/p.frequency:0,first=Math.ceil((.05-offset)/period);return Array.from({length:5},(_,i)=>{const index=first+i,position=featurePosition(p,mode,index),next={...p,position};return measurement(next,mode,index,transfer(next,mode).positionReading,'ideal-model');});}