- estrannaise-cloud.js : paramètre scalePerEster — le nuage reçoit la
MÊME calibration que la courbe ESE (autoByModel ESE). Avant : courbe ESE
calibrée mais nuage BRUT → échelles différentes (remontée v1.9.1 :
« le nuage ne s'active que autour du tracé, pas autour du modèle
Estrannaise »).
- chart.js : autoByModel.get('ESE')?.esterScales passé au nuage ;
DEFAULT_NB_CURVES importé.
- +1 test : nuage calibré ×2 = nuage brut ×2 (167 verts au total).
- CHANGELOG [1.9.1] : fix nuage calibré + HARMONISATION de l'arrondi de
la médiane de calibration (le web avait la troncature historique — les
niveaux web/android convergent ; le niveau correct = l'ARRONDI).
- WEB_VERSION 1.9.1.
86 lines
3.6 KiB
JavaScript
86 lines
3.6 KiB
JavaScript
/**
|
||
* Tests du NUAGE D'INCERTITUDE (v1.9.0 — miroir de `EstrannaiseCloudTest.kt`,
|
||
* `js/pk/estrannaise-cloud.js`) : 32 courbes par défaut, dispersion réelle
|
||
* entre courbes, fenêtre respectée, exclusivité ESE (TFS/oral → vide).
|
||
*/
|
||
|
||
import { test, describe, before } from 'node:test';
|
||
import { join } from 'node:path';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { initProfiles, makeTreatment, makeDose, DAY_MS, WEB_ROOT } from './helpers.js';
|
||
import { compute } from '../js/pk/estrannaise-cloud.js';
|
||
import { initWithJson as initMcmc } from '../js/pk/estrannaise-models.js';
|
||
|
||
const NOW = 1_790_000_000_000;
|
||
|
||
function ese() {
|
||
return makeTreatment({
|
||
name: 'EEn ESE', type: 'ESTRADIOL', route: 'INJECTION_IM',
|
||
doseAmount: 5.0, doseUnit: 'mg', esterType: 'EEN', pkModel: 'ESE',
|
||
forecastIntervalDays: 7.0, isActive: true,
|
||
});
|
||
}
|
||
|
||
function doses() {
|
||
return [4, 3, 2, 1].map((k) => makeDose(NOW - k * DAY_MS, 5.0));
|
||
}
|
||
|
||
describe('EstrannaiseCloud web (miroir Android v1.9.0)', () => {
|
||
before(() => {
|
||
initProfiles();
|
||
// Le nuage exige le posterior MCMC chargé (313 échantillons/ester)
|
||
initMcmc(readFileSync(join(WEB_ROOT, 'assets/mcmc_samples.json'), 'utf8'));
|
||
});
|
||
|
||
test('32 courbes avec dispersion réelle entre elles', () => {
|
||
const cloud = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW);
|
||
assert.equal(cloud.length, 32);
|
||
assert.ok(cloud.every((c) => c.length >= 2));
|
||
const peaks = [...new Set(cloud.map((c) => Math.max(...c.map((p) => p.e2))))];
|
||
assert.ok(peaks.length >= 8, `les courbes divergent (${peaks.length} maxima distincts)`);
|
||
});
|
||
|
||
test('le nuage reste dans la fenêtre demandée', () => {
|
||
const start = NOW - 10 * DAY_MS;
|
||
const end = NOW;
|
||
const cloud = compute([ese()], doses(), start, end, 3_600_000, NOW);
|
||
assert.ok(cloud.every((c) => c.every((p) => p.timestamp >= start && p.timestamp <= end)));
|
||
});
|
||
|
||
test('exclusivité ESE : TFS et oral produisent un nuage vide', () => {
|
||
const tfs = { ...ese(), pkModel: 'TFS' };
|
||
assert.equal(compute([tfs], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0);
|
||
const oral = makeTreatment({
|
||
name: 'CPA', type: 'ANTI_ANDROGEN', route: 'ORAL',
|
||
doseAmount: 12.5, doseUnit: 'mg', esterType: 'NONE',
|
||
forecastIntervalDays: 2.0, isActive: true,
|
||
});
|
||
assert.equal(compute([oral], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0);
|
||
});
|
||
|
||
test('aucune dose sur le porteur → nuage vide', () => {
|
||
assert.equal(compute([ese()], [], NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0);
|
||
});
|
||
|
||
test('nbCurves < 2 → nuage vide', () => {
|
||
assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0);
|
||
});
|
||
|
||
test('le nuage suit la calibration (fix v1.9.1, miroir Android)', () => {
|
||
// scalePerEster ×2 → nuage calibré = nuage brut ×2 (le nuage ENTOURE
|
||
// la courbe ESE calibrée au lieu de flotter à une autre échelle)
|
||
const scaled = { EEN: 2.0 };
|
||
const calibrated = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 32, scaled);
|
||
const raw = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW);
|
||
assert.equal(calibrated.length, raw.length);
|
||
assert.ok(raw[raw.length - 1].length >= 2);
|
||
const rawLast = raw[raw.length - 1][raw[raw.length - 1].length - 1].e2;
|
||
const calLast = calibrated[calibrated.length - 1][calibrated[calibrated.length - 1].length - 1].e2;
|
||
assert.ok(
|
||
Math.abs(calLast - rawLast * 2) <= rawLast * 0.01,
|
||
`calibré (${calLast}) ≈ 2× brut (${rawLast})`,
|
||
);
|
||
});
|
||
});
|