export const MODEL_VERSION='1.0.0',EXPERIMENT_ID='course-01/doppler-verification'; export const DEFAULTS=Object.freeze({frequency:40002,temperature:23.1,velocity:0,distance:.5}); export function validateParameters(p){for(const[k,lo,hi]of[['frequency',20000,60000],['temperature',0,40],['velocity',-1.5,1.5],['distance',.3,.8]])if(!Number.isFinite(p[k])||p[k]hi)throw new RangeError(k);return {...p};} export function soundSpeed(temperature){if(!Number.isFinite(temperature)||temperature<0||temperature>40)throw new RangeError('temperature');return 331.45*Math.sqrt(1+temperature/273.15);} export function response(p){validateParameters(p);const speed=soundSpeed(p.temperature),received=p.frequency*(1+p.velocity/speed);return {speed,received,shift:received-p.frequency,wavelength:speed/p.frequency,period:1/received};} /** Arrival of nth emitted wavefront at receiver x=d-vt. */ export function arrival(p,index){validateParameters(p);if(!Number.isInteger(index)||index<0)throw new RangeError('wave index');const t=(p.distance+soundSpeed(p.temperature)*index/p.frequency)/(soundSpeed(p.temperature)+p.velocity);if(p.distance-p.velocity*t<=0)throw new RangeError('receiver has crossed source');return t;} export function position(p,time){validateParameters(p);if(!Number.isFinite(time)||time<0||time>.25)throw new RangeError('time');return p.distance-p.velocity*time;} export function fit(rows){if(rows.length<3)throw new RangeError('three observations required');if(rows.some(r=>!Number.isFinite(r.velocity)||!Number.isFinite(r.frequency)))throw new RangeError('finite observations');const n=rows.length,mx=rows.reduce((s,r)=>s+r.velocity,0)/n,my=rows.reduce((s,r)=>s+r.frequency,0)/n,xx=rows.reduce((s,r)=>s+(r.velocity-mx)**2,0);if(xx===0)throw new RangeError('distinct velocities');const slope=rows.reduce((s,r)=>s+(r.velocity-mx)*(r.frequency-my),0)/xx,intercept=my-slope*mx;if(slope<=0)throw new RangeError('positive slope');const residuals=rows.map(r=>r.frequency-slope*r.velocity-intercept),variance=residuals.reduce((s,r)=>s+r*r,0)/(n-2);return {slope,intercept,residuals,slopeSE:Math.sqrt(variance/xx)};} export function makeRecord(p,source='automatic-doppler-model'){const r=response(p);if(!['automatic-doppler-model','wavefront-arrival-interval'].includes(source))throw new RangeError('source');return {experimentId:EXPERIMENT_ID,modelVersion:MODEL_VERSION,source,parameters:{...p},group:[p.frequency,p.temperature,source].join('|'),velocity:p.velocity,frequency:source==='wavefront-arrival-interval'?1/(arrival(p,1)-arrival(p,0)):r.received,arrival0:arrival(p,0),arrival1:arrival(p,1),units:{velocity:'m/s',frequency:'Hz',arrival0:'s',arrival1:'s'}};}