Miroir fidèle du Kotlin (doc Android §7.12) :
- estrannaise-models.js : forme close 3C de estrannaise.js (paramètres
publiés EV/EU/EEn/EC/EB/EUCS), posterior MCMC (313 échantillons/ester,
asset mcmc_samples.json ≈ 48 Ko fetché au démarrage — remplace le fetch
des tables ODS, 550 Ko), t½ terminale analytique, mapping en objets
{d,k1,k2,k3} (miroir du Param Kotlin).
- estrannaise-cloud.js : nuage d'incertitude — 32 courbes du posterior
(échelonnées, déterministes) superposant les doses des porteurs ESE ;
exclusif ESE (TFS/WHSAH sans posterior publié), gardes (sans dose,
nbCurves<2, MCMC non chargé → vide).
- pk-engine.js : dispatch ESE → forme close (plus de fallback PKProfileStore
au runtime) ; cutoffHours ESE → 10 × t½ analytique ; lab-timing.js t½ ESE
analytique ; app.js fetch MCMC remplace le fetch des tables ODS
(démarrage plus rapide).
- chart.js/chart-canvas.js : chip « Nuage » (off, activable si ESE affiché,
coupure auto si ESE off), clé 'CLOUD' skippée des légendes (leçon #63),
nuage dessiné en alpha faible, légende dédiée ; seed démo + traitement
EV/ESE (2 doses) pour démontrer le nuage ; choicesForModel ESE=6.
- Tests : estrannaise-models.test.js (6 : fidélité RMS/pics, dégénérés,
MCMC 313×6, t½ analytique, gardes) + estrannaise-cloud.test.js (5 :
32 courbes à dispersion réelle, fenêtre, exclusivité ESE, gardes)
→ 166 verts + E2E (chip/légende/exclusivité nuage) + check --release.
- WEB_VERSION 1.9.0 (versions sync Android). CHANGELOG [1.9.0] web.
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|