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.
128 lines
5.8 KiB
JavaScript
128 lines
5.8 KiB
JavaScript
/**
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
* 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);
|
||
}
|