export const MODEL_VERSION='1.0.0'; export const DEFAULT_PARAMETERS=Object.freeze({solidDensity:7.9,solidVolume:5.67,liquidDensity:1.08,bottleVolume:16,emptyMass:31.63,waterDensity:.99779,immersion:1}); export const PARAMETER_BOUNDS=Object.freeze({solidDensity:[1.1,12],solidVolume:[1,20],liquidDensity:[.5,1.5],bottleVolume:[5,50],emptyMass:[5,100],waterDensity:[.99,1.005],immersion:[0,1]}); 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 idealMass(p,object){validateParameters(p);switch(object){case'air':return p.solidDensity*p.solidVolume;case'submerged':return(p.solidDensity-p.waterDensity*p.immersion)*p.solidVolume;case'empty':return p.emptyMass;case'water':return p.emptyMass+p.waterDensity*p.bottleVolume;case'liquid':return p.emptyMass+p.liquidDensity*p.bottleVolume;default:throw new RangeError('unknown object');}} export function inferSolid(air,submerged,waterDensity){if(![air,submerged,waterDensity].every(Number.isFinite)||!(air>submerged&&submerged>0&&waterDensity>0))throw new RangeError('须满足空气质量 > 水中视质量 > 0');const volume=(air-submerged)/waterDensity;return{density:air/volume,volume};} export function inferLiquid(empty,water,liquid,waterDensity){if(![empty,water,liquid,waterDensity].every(Number.isFinite)||!(empty>=0&&water>empty&&liquid>empty&&waterDensity>0))throw new RangeError('水和待测液体总质量均须大于空瓶质量');const volume=(water-empty)/waterDensity;return{density:(liquid-empty)/volume,volume};} export function sensitivitySolid(air,submerged,waterDensity){inferSolid(air,submerged,waterDensity);const d=air-submerged;return{air:-waterDensity*submerged/d**2,submerged:waterDensity*air/d**2,waterDensity:air/d};} export function sensitivityLiquid(empty,water,liquid,waterDensity){inferLiquid(empty,water,liquid,waterDensity);const d=water-empty;return{empty:waterDensity*(liquid-water)/d**2,water:-waterDensity*(liquid-empty)/d**2,liquid:waterDensity/d,waterDensity:(liquid-empty)/d};} export function sceneKey(p){validateParameters(p);return JSON.stringify(Object.fromEntries(Object.keys(PARAMETER_BOUNDS).filter(k=>k!=='immersion').map(k=>[k,p[k]])));} export function analyzeRecords(rows,p,source){const key=sceneKey(p),same=rows.filter(r=>r.scene===key&&r.source===source),last={};for(const r of same)last[r.object]=r;const result={solid:null,liquid:null};if(last.air&&last.submerged){if(last.submerged.parameters.immersion!==1)result.solidError='水中称量须完全浸没;当前末次水中记录未满足。';else result.solid=inferSolid(last.air.mass,last.submerged.mass,p.waterDensity);}if(last.empty&&last.water&&last.liquid)result.liquid=inferLiquid(last.empty.mass,last.water.mass,last.liquid.mass,p.waterDensity);return result;} export function measurement(p,object,mass,source){validateParameters(p);if(!Number.isFinite(mass)||mass<0||mass>500)throw new RangeError('砝码质量须在0–500 g');if(!['manual-balance','ideal-balance'].includes(source))throw new RangeError('unknown source');idealMass(p,object);return{experiment:'course-01/density',modelVersion:MODEL_VERSION,scene:sceneKey(p),parameters:{...p},object,mass,unit:'g',source};}