/** * Tests du modèle **Estrannaise analytique** (v1.9.0 — miroir de * `EstrannaiseModelsTest.kt`) : forme close 3C publiée par estrannaise.js, * fidélité aux tables ODS (RMS ≈ 0), cas dégénérés, MCMC 313, t½ analytique. */ import { test, describe, before } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { initProfiles, WEB_ROOT } from './helpers.js'; import { sample, hasModel, model, mcmcSamples, initWithJson, sampleWithParams, } from '../js/pk/estrannaise-models.js'; describe('EstrannaiseModels web (miroir Android v1.9.0)', () => { before(() => { initProfiles(); // tables ODS (référence de fidélité, src/test side) const mcmc = readFileSync(join(WEB_ROOT, 'assets/mcmc_samples.json'), 'utf8'); initWithJson(mcmc); }); test('FIDÉLITÉ : la forme close reproduit les tables ODS (RMS ≈ 0)', () => { const profiles = JSON.parse( readFileSync(join(WEB_ROOT, 'assets/pk_profiles.json'), 'utf8'), ).profiles; for (const ester of ['EV', 'EU', 'EEN']) { const key = Object.keys(profiles) .find((k) => k.toLowerCase().startsWith(ester.toLowerCase() + '_') && k.toLowerCase().endsWith('ese')); const arr = profiles[key]; let maxAbs = 0; let sumSq = 0; for (let h = 0; h <= 4800; h++) { const diff = sample(ester, h) - arr[h]; maxAbs = Math.max(maxAbs, Math.abs(diff)); sumSq += diff * diff; } const rms = Math.sqrt(sumSq / 4801); assert.ok(maxAbs <= 0.01 && rms <= 0.01, `${ester} : maxAbs=${maxAbs} rms=${rms}`); } }); test('les pics publiés sont reproduits (61,12 / 3,44 / 31,35 pg/mL/mg)', () => { const expects = [ ['EV', 61.12], ['EU', 3.44], ['EEN', 31.35], ]; for (const [ester, peakExpect] of expects) { let peak = 0; for (let t = 0; t <= 200; t += 0.25 / 24) { peak = Math.max(peak, sample(ester, t * 24)); } assert.ok( Math.abs(peak - peakExpect) <= peakExpect * 0.002, `pic ${ester} = ${peak} (attendu ${peakExpect} ±0,2 %)`, ); } }); test('taux dégénérés : solutions limites sans NaN ni négatif', () => { for (const p of [ { d: 478, k1: 0.236, k2: 0.236, k3: 1.24 }, { d: 478, k1: 0.236, k2: 1.24, k3: 0.236 }, { d: 478, k1: 0.236, k2: 1.24, k3: 1.24 }, { d: 478, k1: 0.5, k2: 0.5, k3: 0.5 }, ]) { for (const t of [0.5, 1.0, 24.0, 240.0, 2400.0]) { const v = sampleWithParams('EV', t, p); assert.ok(v >= 0 && !Number.isNaN(v), `finie et ≥ 0 (t=${t})`); } } }); test('MCMC : 313 échantillons par ester injectable (6 esters)', () => { for (const ester of ['EV', 'EU', 'EEN', 'EC', 'EB', 'EUCS']) { assert.equal(mcmcSamples(ester).length, 313, `posterior ${ester}`); assert.notEqual( mcmcSamples(ester)[0].d, mcmcSamples(ester)[312].d, 'le posterior couvre une plage', ); } }); test('t½ terminale ANALYTIQUE : EV = ln2/0,236 ≈ 2,94 j', () => { const tHalf = model('EV').terminalHalfLifeDays; assert.ok(Math.abs(tHalf - Math.log(2) / 0.236) < 1e-12); }); test('normalisation par mg + gardes (dt ≤ 0, ester inconnu, EUCS couvert)', () => { assert.equal(sample('EEN', 0), 0); assert.equal(sample('EEN', -1), 0); assert.equal(sample('XXX', 24), 0); assert.ok(sample('EUCS', 24) > 0); assert.equal(hasModel('XXX'), false); assert.equal(hasModel('EUCS'), true); }); });