HormoneTrack-web/js/pk/whsah-models.js
Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
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.
2026-09-08 14:34:58 +02:00

128 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ─────────────────────────────────────────────────────────────────────────────
* 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);
}