Portage navigateur complet de l'app Android HormoneTrack v1.4.10 : - moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C, WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés - calibration par période d'ester et par modèle affiché (#60/#61) - UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire, prévision, pics/creux, fuseau configurable), rappels web (Notification API), seuils d'alerte, i18n FR/EN - sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1) - 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie - processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur, smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG) - dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions alignées sur l'Android porté, changelogs indépendants Non porté (impossible dans un navigateur, documenté §12) : agenda récurrent, notifications onglet fermé, montre.
66 lines
2.3 KiB
JavaScript
66 lines
2.3 KiB
JavaScript
/**
|
||
* Fidélité du modèle WHSAH au comportement de Mona
|
||
* (miroir de WhsahModelsTest.kt — fit « license-free » du WHSAH Collective).
|
||
*
|
||
* Valeurs épinglées (±2 %) : EV Cmax 367,5 @ 1,69 j · EEn 187,9 @ 4,99 j ·
|
||
* t½ EEn 7,34 j · PEP NON COUVERT · linéarité par mg.
|
||
*/
|
||
|
||
import { test, describe } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { assertClose } from './helpers.js';
|
||
import * as WHS from '../js/pk/whsah-models.js';
|
||
|
||
function findPeak(ester, doseMg, maxDays) {
|
||
let best = { v: 0, tDays: 0 };
|
||
for (let h = 0; h <= maxDays * 24; h += 0.25) {
|
||
const v = WHS.sample(ester, h) * doseMg;
|
||
if (v > best.v) best = { v, tDays: h / 24 };
|
||
}
|
||
return best;
|
||
}
|
||
|
||
describe('WhsahModels (fit Mona, license-free)', () => {
|
||
test('pics Mona : EV 367,5 @ 1,69 j (5 mg) · EEn 187,9 @ 4,99 j (5 mg)', () => {
|
||
const ev = findPeak('EV', 5, 10);
|
||
assertClose(ev.v, 367.5, 0.02, 'pic EV WHSAH 5 mg');
|
||
assertClose(ev.tDays, 1.69, 0.1, 'Tmax EV WHSAH');
|
||
|
||
const een = findPeak('EEN', 5, 20);
|
||
assertClose(een.v, 187.9, 0.02, 'pic EEn WHSAH 5 mg');
|
||
assertClose(een.tDays, 4.99, 0.15, 'Tmax EEn WHSAH');
|
||
});
|
||
|
||
test('t½ terminale EEn ≈ 7,34 j (plus longue que TFS 4,5 j)', () => {
|
||
assertClose(WHS.model('EEN').terminalHalfLifeDays, 7.34, 0.05);
|
||
});
|
||
|
||
test('EEn à J+1 ~3× plus haut que TFS (70 vs 22 pg/mL) — la « montée rapide » WHSAH', () => {
|
||
// Sémantique épinglée côté Android (différence visible entre les apps)
|
||
const eenJ1 = WHS.sample('EEN', 24) * 5;
|
||
assertClose(eenJ1, 70, 0.15, 'EEn WHSAH à J+1 (5 mg)');
|
||
});
|
||
|
||
test('PEP non couvert → 0 (retiré du fit WHSAH)', () => {
|
||
assert.equal(WHS.sample('PEP', 100), 0);
|
||
assert.equal(WHS.hasModel('PEP'), false);
|
||
});
|
||
|
||
test('linéarité par mg', () => {
|
||
const a = WHS.sample('EV', 40);
|
||
assertClose(a * 3, WHS.sample('EV', 40) * 3, 1e-12);
|
||
});
|
||
|
||
test('6 esters couverts (sans PEP) — choicesForModel en dépend', () => {
|
||
for (const e of ['EV', 'EU', 'EEN', 'EB', 'EC', 'ECS']) {
|
||
assert.ok(WHS.hasModel(e), `${e} devrait être couvert`);
|
||
}
|
||
});
|
||
|
||
test('dt ≤ 0 → 0 ; ester inconnu → 0', () => {
|
||
assert.equal(WHS.sample('EV', 0), 0);
|
||
assert.equal(WHS.sample('EV', -3), 0);
|
||
assert.equal(WHS.sample('XXX', 40), 0);
|
||
});
|
||
});
|