/** * ───────────────────────────────────────────────────────────────────────────── * Modèle PK « WHSAH » (portage web de `pk/WhsahModels.kt`, v1.4.6) — 3ᵉ modèle * au niveau d'Estrannaise et Transfem Science, superposable sur le graphique. * * SOURCE : le fit « license-free » publié par le WHSAH Collective dans l'app * open-source Mona (projet Flutter — lib/data/model/graph_calculator.dart, * commit « new license-free pk params » ; leurs auteurs déclarent explicitement * les coefficients libres d'usage, « Inferred with love exclusively for Mona »). * Crédit : alix / WHSAH Collective via Mona. * * POURQUOI CE MODÈLE EXISTE : Mona a d'abord utilisé les paramètres Transfem * Science (identiques aux nôtres) PUIS les a remplacés par ce fit indépendant, * pour des raisons de licence. Les deux sont des tri-exponentielles en forme * close ajustées sur les mêmes études, avec des conventions différentes : * * - WHSAH paramètre une BIODISPONIBILITÉ explicite F < 1 (0,62–0,76) et un * facteur d'échelle `auc` intégré — les courbes « brutes » sont ~24 % plus * basses que TFS non calibrée (la calibration de l'app absorbe ça) ; * - la MONTÉE est plus rapide (compartiment rapide plus vif) et la t½ * terminale plus longue sur certains esters : EV Cmax 367 pg/mL @ 1,7 j * (TFS : 295 @ 2,1 j), EEn Cmax 188 @ 5,0 j (TFS : 160 @ 6,5 j) et t½ EEn * 7,3 j (TFS : 4,5 j) — à J+1 l'EEn WHSAH est ~3× plus haut (70 vs 22 pg/mL) ; * - PEP NON COUVERT (retiré du jeu WHSAH) → `sample('PEP', …) = 0`. * * FORMULE (telle qu'implémentée dans Mona, normalisée PAR MG — le * multiplicateur de dose est appliqué par le moteur PK) : * * sample = F · auc · k1·k2·k3 · [ e^(−k1·t)/((k1−k2)(k1−k3)) * − e^(−k2·t)/((k1−k2)(k2−k3)) * + e^(−k3·t)/((k1−k3)(k2−k3)) ] * * avec t en JOURS, résultat en pg/mL pour 1 mg. Sanity checks épinglés par * web/tests/whsah-models.test.js (fidélité au comportement de Mona, ±2 %) : * EV Cmax 367,5 @ 1,69 j · EEn 187,9 @ 4,99 j · t½ EEn 7,34 j. * ───────────────────────────────────────────────────────────────────────────── */ /** Modèle WHSAH d'un ester (fit F/auc/k1/k2/k3). */ export class WHS { /** * @param {string} ester Clé ester de l'app ("EV", "EB", …) * @param {number} F Biodisponibilité explicite (< 1 dans le fit WHSAH) * @param {number} auc Constante d'échelle du fit (pg/mL intégrés) * @param {number} k1 Constante de vitesse j⁻¹ (absorption) * @param {number} k2 Constante de vitesse j⁻¹ (phase rapide) * @param {number} k3 Constante de vitesse j⁻¹ (phase lente / terminale) */ constructor(ester, F, auc, k1, k2, k3) { this.ester = ester; this.F = F; this.auc = auc; this.k1 = k1; this.k2 = k2; this.k3 = k3; } /** Demi-vie TERMINALE (jours) = la phase la plus lente des trois. */ get terminalHalfLifeDays() { return Math.log(2.0) / Math.min(this.k1, this.k2, this.k3); } } /** Table des modèles WHSAH par clé ester (identique au Kotlin — ne pas éditer sans ré-épingler les tests). */ const MODELS = new Map([ // estradiol enanthate (solution huileuse) ['EEN', new WHS('EEN', 0.708, 875.4, 0.09441, 3.354, 0.4078)], // estradiol valerate (solution huileuse) ['EV', new WHS('EV', 0.764, 621.3, 0.2230, 17.62, 1.305)], // estradiol benzoate (solution huileuse) ['EB', new WHS('EB', 0.723, 889.9, 0.5220, 521.9, 5.223)], // estradiol cypionate — SOLUTION HUILEUSE ['EC', new WHS('EC', 0.687, 554.5, 0.0880, 17.95, 0.7177)], // estradiol cypionate — SUSPENSION AQUEUSE (microcristaux) ['ECS', new WHS('ECS', 0.687, 852.6, 0.0973, 218.67, 6.624)], // estradiol undecylate — données d'étude limitées (comme chez TFS) ['EU', new WHS('EU', 0.618, 385.8, 0.02189, 183.4, 1.564)], ]); /** * @param {string} ester Clé ester — lookup insensible à la casse * @returns {boolean} true si un modèle WHSAH existe pour cet ester */ export function hasModel(ester) { return MODELS.has(String(ester).toUpperCase()); } /** * @param {string} ester Clé ester (lookup insensible à la casse) * @returns {WHS|null} le modèle WHSAH de l'ester, ou null */ export function model(ester) { return MODELS.get(String(ester).toUpperCase()) || null; } /** * Réponse normalisée (pg/mL PAR mg) à dtHours après une injection de 1 mg, * modèle WHSAH. * * @param {string} ester Clé ester ("EV", …) * @param {number} dtHours Heures écoulées depuis l'injection (≤ 0 → 0) * @returns {number} pg/mL par mg */ export function sample(ester, dtHours) { if (dtHours <= 0.0) return 0.0; const m = model(ester); if (!m) return 0.0; return rawConcentrationDays(m, dtHours / 24.0); // t en jours } /** * Cp(t) en pg/mL pour 1 mg (forme Mona exacte — voir KDoc de l'objet). * * Les compartiments « ultra-rapides » (k2 jusqu'à ~520 j⁻¹ : EB) décroissent * instantanément et n'affectent que t≈0 — double précision suffisante * (vérifié par les tests de pics). * * @param {WHS} m Modèle de l'ester * @param {number} tDays Temps en JOURS depuis l'injection * @returns {number} pg/mL pour 1 mg */ function rawConcentrationDays(m, tDays) { const part1 = Math.exp(-m.k1 * tDays) / ((m.k1 - m.k2) * (m.k1 - m.k3)); const part2 = Math.exp(-m.k2 * tDays) / ((m.k1 - m.k2) * (m.k2 - m.k3)); const part3 = Math.exp(-m.k3 * tDays) / ((m.k1 - m.k3) * (m.k2 - m.k3)); return m.F * m.auc * m.k1 * m.k2 * m.k3 * (part1 - part2 + part3); }