Download tests/model.test.mjs from skailiner/threshold: direct link, hf CLI and curl.
- Browser
- Download file 6.83 kB
-
https://huggingface.co/spaces/skailiner/threshold/resolve/main/tests/model.test.mjs
- Command line
-
hf download hf://spaces/skailiner/threshold/tests/model.test.mjs
-
curl -L -o model.test.mjs https://huggingface.co/spaces/skailiner/threshold/resolve/main/tests/model.test.mjs
6.83 kB
| import assert from 'node:assert/strict'; | |
| import { climates, defaults, baseline, configure, validateDesign, solarAt, beamShadeFraction, | |
| thermalStep, ventilationAt, simulateDay, summarize, comparisonText, ROOM } from '../build/model.mjs'; | |
| let count = 0; | |
| function test(name, run) { run(); count++; console.log('PASS', name); } | |
| const near = (a,b,tolerance=1e-8) => assert.ok(Math.abs(a-b) <= tolerance, `${a} differs from ${b} by more than ${tolerance}`); | |
| test('Exact thermal integration and energy conservation', () => { | |
| const input = { inside: 20, outside: 35, conductance: 90, capacity: 1e6, gains: 200, seconds: 600 }; | |
| const result = thermalStep(input); | |
| const equilibrium = 35 + 200/90; | |
| near(result.temperature, equilibrium + (20-equilibrium)*Math.exp(-90*600/1e6)); | |
| near(result.residualJ, 0, 1e-6); | |
| near(thermalStep({...input, inside:25, outside:25, gains:0}).temperature, 25); | |
| near(thermalStep({...input, conductance:0}).temperature, 20.12); | |
| }); | |
| test('Heat flow is signed and thermal storage slows changes', () => { | |
| const input = { inside:25, outside:35, conductance:50, capacity:1e6, gains:0, seconds:600 }; | |
| assert.ok(thermalStep({...input, conductance:150}).temperature > thermalStep(input).temperature); | |
| assert.ok(thermalStep({...input, outside:15, conductance:150}).temperature < thermalStep({...input,outside:15}).temperature); | |
| assert.ok(thermalStep({...input,capacity:5e6}).temperature < thermalStep(input).temperature); | |
| }); | |
| test('Night solar is zero for every facade and climate', () => { | |
| for (const climate of Object.values(climates)) for (let orientation=0;orientation<=315;orientation+=45) { | |
| for (const hour of [0,1,3,21,23]) { | |
| const s = solarAt(climate,{...defaults,orientation},hour); | |
| assert.equal(s.watts,0); assert.equal(s.beam,0); assert.equal(s.diffuse,0); | |
| } | |
| for(let hour=0;hour<24;hour+=0.125) { | |
| const s=solarAt(climate,{...defaults,orientation},hour); | |
| assert.ok(Number.isFinite(s.watts) && s.watts>=0); | |
| } | |
| } | |
| }); | |
| test('Horizontal overhang blocks more direct beam at high sun and greater depth', () => { | |
| assert.ok(beamShadeFraction(.9,68,0)>beamShadeFraction(.9,30,0)); | |
| assert.ok(beamShadeFraction(1.8,30,0)>beamShadeFraction(.9,30,0)); | |
| assert.equal(beamShadeFraction(0,68,0),0); | |
| assert.equal(beamShadeFraction(2.4,-3,0),0); | |
| for (const climate of Object.values(climates)) for(let hour=0;hour<24;hour+=.25) { | |
| assert.ok(solarAt(climate,{...defaults,shade:2.4},hour).watts <= solarAt(climate,{...defaults,shade:0},hour).watts + 1e-8); | |
| } | |
| }); | |
| test('Purge boundaries, proportional ramp and actual infiltration', () => { | |
| for(const [outdoor,inside] of [[20,25],[24.5,25],[26,26.5],[35,25]]) { | |
| const v=ventilationAt(climates.humid,defaults,outdoor,inside); | |
| assert.equal(v.purge,false); near(v.ach,ROOM.infiltrationACH); | |
| } | |
| const partial=ventilationAt(climates.humid,defaults,24,25.25); | |
| const full=ventilationAt(climates.humid,defaults,24,25.5); | |
| near(partial.openingFraction,.5); near(full.openingFraction,1); | |
| assert.ok(full.ach>partial.ach); | |
| }); | |
| test('Complete configuration validation is atomic and rejects coercion', () => { | |
| const original={design:{...defaults},climate:'humid',hour:16,lens:'heat'}; | |
| const frozen=JSON.stringify(original); | |
| for(const input of [ | |
| {shade:1.5,lens:'invalid'}, {glazing:20,climate:'nowhere'}, {mass:50,hour:24}, | |
| {shade:null},{mass:true},{glazing:'34'},{hour:NaN},{unknown:1},{orientation:135.1},[], | |
| ]) { | |
| assert.throws(()=>configure(original,input)); | |
| assert.equal(JSON.stringify(original),frozen); | |
| } | |
| const changed=configure(original,{shade:1.5,climate:'arid',hour:23,lens:'air'}); | |
| assert.equal(changed.design.shade,1.5); assert.equal(changed.hour,23); | |
| assert.equal(JSON.stringify(original),frozen); | |
| assert.deepEqual(validateDesign(defaults),defaults); | |
| }); | |
| test('Occupied duration is exactly twelve hours; gain integral has correct units', () => { | |
| const s=simulateDay(climates.humid,defaults); | |
| const result=summarize(s); | |
| assert.equal(s.steps.filter(x=>x.occupied).length,72); | |
| assert.equal(result.occupiedHours,12); | |
| const constant={...s,steps:s.steps.map(row=>({...row,solarW:100}))}; | |
| near(summarize(constant).solarKWh,2.4); | |
| assert.equal(s.hourly.length,24); | |
| }); | |
| test('Periodic convergence is independent of starting temperature', () => { | |
| for(const climate of Object.values(climates)) for(const design of [defaults,baseline,{...defaults,mass:100,ventilation:5}]) { | |
| const cold=simulateDay(climate,design,{initialTemperature:5}); | |
| const hot=simulateDay(climate,design,{initialTemperature:50}); | |
| assert.ok(cold.converged && hot.converged); | |
| near(summarize(cold).peak.temperature,summarize(hot).peak.temperature,.02); | |
| for(let i=0;i<cold.steps.length;i++) near(cold.steps[i].temperature,hot.steps[i].temperature,.02); | |
| } | |
| const unsettled=simulateDay(climates.humid,defaults,{maximumDays:1,initialTemperature:5}); | |
| assert.equal(unsettled.converged,false); assert.equal(unsettled.warmupDays,1); | |
| }); | |
| test('Ten-minute steps agree with five-minute steps within declared educational tolerance', () => { | |
| for(const climate of Object.values(climates)) for(const design of [defaults,baseline,{...defaults,mass:0,ventilation:100}]) { | |
| const ten=summarize(simulateDay(climate,design)); | |
| const five=summarize(simulateDay(climate,design,{minutes:5})); | |
| near(ten.peak.temperature,five.peak.temperature,.2); | |
| near(ten.bandHours,five.bandHours,.5); | |
| near(ten.meanACH,five.meanACH,.2); | |
| near(ten.purgeHours,five.purgeHours,.5); | |
| } | |
| }); | |
| test('Design grid remains finite, deterministic, energy-balanced and converged', () => { | |
| let cases=0; | |
| for(const climate of Object.values(climates)) for(const orientation of [0,90,180,270]) | |
| for(const glazing of [12,72]) for(const shade of [0,2.4]) | |
| for(const mass of [0,100]) for(const ventilation of [5,100]) { | |
| const design={orientation,glazing,shade,mass,ventilation}; | |
| const s=simulateDay(climate,design); | |
| assert.ok(s.converged,JSON.stringify({climate:climate.name,design})); | |
| assert.ok(s.steps.every(r=>Number.isFinite(r.temperature) && r.ach>=.35 && r.solarW>=0)); | |
| assert.ok(s.steps.every(r=>Math.abs(r.residualJ)<1e-5)); | |
| assert.ok(summarize(s).bandPercent>=0 && summarize(s).bandPercent<=100); | |
| cases++; | |
| } | |
| assert.equal(cases,192); | |
| assert.deepEqual(simulateDay(climates.humid,defaults),simulateDay(climates.humid,defaults)); | |
| }); | |
| test('Worse schemes are reported as higher, never as zero improvement', () => { | |
| const reference={peak:{temperature:30}}; | |
| assert.match(comparisonText({peak:{temperature:32}},reference),/2.0°C higher/); | |
| assert.match(comparisonText({peak:{temperature:28}},reference),/2.0°C lower/); | |
| }); | |
| console.log(`\n${count} test groups passed, including 192 parameter-grid cases.`); | |