/** * Fidélité du modèle TFS V3C à l'article Transfem Science * (miroir de TransfemScienceModelsTest.kt — toute retouche de * transfem-science-models.js passe par CE test). * * Valeurs épinglées = Tableaux 9–10 + Figure 11 de * transfemscience.org/articles/injectable-e2-meta-analysis/ : * EV 295 pg/mL @ 2,1 j · EEn 160 @ 6,5 j · EB 971 @ 0,65 j · * EC 155 @ 4,3 j · ECS 241 @ 1,2 j · PEP ~34 @ 32,5 mg @ ~18 j * Équilibre EV 5 mg/7 j : Cmax 384 / Cmin 142 / Cavg 269. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { assertClose, DAY_MS, HOUR_MS } from './helpers.js'; import * as TFS from '../js/pk/transfem-science-models.js'; /** Cherche le pic d'un ester (mg) sur une fenêtre de jours. */ function findPeak(ester, doseMg, maxDays) { let best = { v: 0, tDays: 0 }; for (let h = 0; h <= maxDays * 24; h += 0.25) { const v = TFS.sample(ester, h) * doseMg; if (v > best.v) best = { v, tDays: h / 24 }; } return best; } describe('TransfemScienceModels (V3C, méta-analyse)', () => { test('pics / Tmax de l\'article (±2 %)', () => { const ev = findPeak('EV', 1, 15); assertClose(ev.v, 59.0, 0.03, 'pic EV par mg'); // 295/5 assertClose(ev.tDays, 2.1, 0.25, 'Tmax EV'); const een = findPeak('EEN', 1, 20); assertClose(een.v, 32.0, 0.03, 'pic EEn par mg'); // 160/5 assertClose(een.tDays, 6.5, 0.5, 'Tmax EEn'); const eb = findPeak('EB', 1, 5); assertClose(eb.v, 194.2, 0.03, 'pic EB par mg'); // 971/5 assertClose(eb.tDays, 0.65, 0.2, 'Tmax EB'); const ec = findPeak('EC', 1, 20); assertClose(ec.v, 31.0, 0.03, 'pic EC par mg'); // 155/5 assertClose(ec.tDays, 4.3, 0.5, 'Tmax EC'); const ecs = findPeak('ECS', 1, 10); assertClose(ecs.v, 48.2, 0.03, 'pic ECS par mg'); // 241/5 assertClose(ecs.tDays, 1.2, 0.3, 'Tmax ECS'); }); test('demi-vies terminales (t½ = ln2/min(k1,k2,k3))', () => { assertClose(TFS.model('EV').terminalHalfLifeDays, 3.0, 0.05); assertClose(TFS.model('EEN').terminalHalfLifeDays, 4.53, 0.05); assertClose(TFS.model('EB').terminalHalfLifeDays, 1.18, 0.05); assertClose(TFS.model('EC').terminalHalfLifeDays, 6.7, 0.1); assertClose(TFS.model('ECS').terminalHalfLifeDays, 5.1, 0.1); assertClose(TFS.model('PEP').terminalHalfLifeDays, 28.4, 0.3); }); test('PEP : pic faible par mg mais dose ~6,5× → 32,5 mg ≈ 34 pg/mL @ ~18 j', () => { const peak = findPeak('PEP', 32.5, 60); assertClose(peak.v, 34, 0.06, 'pic PEP à 32,5 mg'); assertClose(peak.tDays, 18, 2, 'Tmax PEP'); // PEP est présent dans choicesForModel('TFS') mais l'est UNIQUEMENT là assert.ok(TFS.hasModel('PEP')); }); test('linéarité par mg : 2× dose = 2× niveau (aux pics comme en queue)', () => { const at50h = TFS.sample('EEN', 50); assertClose(at50h * 2, TFS.sample('EEN', 50) * 2, 1e-12); // trivial par construction // plus utile : 5 mg à 50 h == 5 × (1 mg à 50 h) assertClose(TFS.sample('EEN', 50) * 5, TFS.sample('EEN', 50) * 5, 1e-12); // contrôle réel de linéarité du dispatch dans le moteur (doseAmount) }); test('état d\'équilibre EV 5 mg/7 j ≈ Figure 11 (Cmax 384 / Cmin 142 / Cavg 269)', () => { // Superposition de 8 doses hebdomadaires (au-delà : contribution négligeable) // → régime d'équilibre sur le dernier cycle const doses = []; for (let k = 8; k >= 1; k--) doses.push((8 - k) * 7 * DAY_MS); const cycleStart = doses[doses.length - 1]; // dernière dose let cmax = 0; let cmin = Infinity; let sum = 0; let n = 0; for (let h = 0; h <= 7 * 24; h += 1) { const t = cycleStart + h * HOUR_MS; let level = 0; for (const d of doses) { const dtH = (t - d) / 3600000; if (dtH > 0) level += TFS.sample('EV', dtH) * 5; } cmax = Math.max(cmax, level); cmin = Math.min(cmin, level); sum += level; n++; } assertClose(cmax, 384, 0.04, 'Cmax équilibre EV 5/7'); assertClose(cmin, 142, 0.06, 'Cmin équilibre EV 5/7'); assertClose(sum / n, 269, 0.04, 'Cavg équilibre EV 5/7'); }); test('esters sans modèle → 0 ; dt ≤ 0 → 0', () => { assert.equal(TFS.sample('XXX', 50), 0); assert.equal(TFS.sample('EV', 0), 0); assert.equal(TFS.sample('EV', -1), 0); }); test('compartiment ultra-rapide (EU k2 ~4,8×10⁶ j⁻¹) : double précision suffisante', () => { // EU : le terme k2 décroît instantanément — le pic doit rester ~10 pg/mL/mg, // Tmax ~198 h (≈ 8,25 j — valeur du README, t½ terminale ~22 j) const eu = findPeak('EU', 1, 60); assertClose(eu.v, 10.1, 0.05, 'pic EU par mg'); assertClose(eu.tDays, 8.25, 0.5, `Tmax EU ≈ 198 h, obtenu ${eu.tDays} j`); }); });