export const MODEL_VERSION='1.0.0',EXPERIMENT_ID='course-04/optical-rotation',DEFAULTS=Object.freeze({specific_rotation:66.46,tube_length_dm:2,concentration_g_ml:.1,half_angle_deg:5}); export function validateParameters(p){for(const[k,l,h]of[['specific_rotation',60,70],['tube_length_dm',.5,3],['concentration_g_ml',.01,.2],['half_angle_deg',2,10]])if(!Number.isFinite(p[k])||p[k]h)throw new RangeError(`${k}须在${l}–${h}`);return{...p};} export function rotation(p){validateParameters(p);return p.specific_rotation*p.tube_length_dm*p.concentration_g_ml;} export function halfField(analyzer_deg,p,blank=false){validateParameters(p);if(!Number.isFinite(analyzer_deg)||analyzer_deg<0||analyzer_deg>180)throw new RangeError('检偏器角0–180°');const phi=blank?0:rotation(p),rad=Math.PI/180;return{left:Math.cos((analyzer_deg-phi-p.half_angle_deg)*rad)**2,right:Math.cos((analyzer_deg-phi+p.half_angle_deg)*rad)**2,rotation_deg:phi};} export function difference180(after,before){if(!Number.isFinite(after)||!Number.isFinite(before))throw new RangeError('角度读数须有限');const d=((after-before+90)%180+180)%180-90;if(Math.abs(d+90)<1e-10)throw new RangeError('90°分支方向不唯一');return d;} export function measurement(p,source='ideal-model',blank_angle=null,sample_angle=null){validateParameters(p);if(!['ideal-model','manual-equal-field'].includes(source))throw new RangeError('记录身份无效');const before=source==='ideal-model'?90:blank_angle,after=source==='ideal-model'?90+rotation(p):sample_angle,phi=difference180(after,before);return{experiment_id:EXPERIMENT_ID,model_version:MODEL_VERSION,parameters:{...p},source,blank_angle_deg:before,sample_angle_deg:after,rotation_deg:phi,concentration_g_ml:phi/(p.specific_rotation*p.tube_length_dm)};}