export const MODEL_VERSION='1.0.0',EXPERIMENT_ID='course-04/thin-lens'; export const DEFAULTS=Object.freeze({focalLength:.10008427569062277,objectRatio:2,screenRatio:5,lensFraction:.5,height:.005}); export function validateParameters(p){for(const[k,lo,hi]of[['focalLength',.05,.2],['objectRatio',.5,4],['screenRatio',3,8],['lensFraction',.1,.9],['height',.001,.01]])if(!Number.isFinite(p[k])||p[k]hi)throw new RangeError(k);return {...p};} export function conjugates(f,u){if(!Number.isFinite(f)||!Number.isFinite(u)||f<=0||u<=0)throw new RangeError('positive f and u required');if(Math.abs(u-f)<=1e-12*Math.max(f,u))return {u,v:null,magnification:null,type:'collimated'};const v=f*u/(u-f);return {u,v,magnification:-v/u,type:v>0?'real':'virtual'};} export function bessel(f,D){if(!Number.isFinite(f)||!Number.isFinite(D)||f<=0||D<=0)throw new RangeError('positive f and D required');const discriminant=D*(D-4*f),eps=1e-12*Math.max(D*D,4*D*f);if(discriminant < -eps)return {status:'no-real-position',positions:[],separation:null};const gap=Math.sqrt(Math.max(0,discriminant));return {status:gap===0?'single-position':'two-positions',positions:[(D-gap)/2,(D+gap)/2],separation:gap};} export function focalFromConjugates(u,v){if(!Number.isFinite(u)||!Number.isFinite(v)||u<=0||v===0||u+v===0)throw new RangeError('invalid distances');return u*v/(u+v);} export function focalFromBessel(D,d){if(!Number.isFinite(D)||!Number.isFinite(d)||D<=0||d<0||d>=D)throw new RangeError('invalid Bessel measurements');return (D*D-d*d)/(4*D);} export function traceRay(f,u,screen,height,angle){if(![f,u,screen,height,angle].every(Number.isFinite)||f<=0||u<=0||screen<0)throw new RangeError('ray parameters');const lensHeight=height+u*angle,outAngle=angle-lensHeight/f;return {lensHeight,outAngle,screenHeight:lensHeight+screen*outAngle};} export function makeRecord(p,method='conjugates',role='sample'){validateParameters(p);if(!['conjugates','bessel'].includes(method)||!['sample','first','second'].includes(role))throw new RangeError('record method');const D=p.screenRatio*p.focalLength,u=method==='conjugates'?p.objectRatio*p.focalLength:D*p.lensFraction,c=conjugates(p.focalLength,u);if(method==='conjugates'&&c.v===null)throw new RangeError('collimated beam has no finite image');return {experimentId:EXPERIMENT_ID,modelVersion:MODEL_VERSION,source:'ideal-paraxial-model',method,role,parameters:{...p},group:[method,p.focalLength,D].join('|'),u,v:c.v,D,lensPosition:u,focalLength:method==='conjugates'?focalFromConjugates(u,c.v):null,units:{u:'m',v:'m',D:'m',lensPosition:'m',focalLength:'m'}};}