HormoneTrack-web/js/pk/estrannaise-models.js
Siphonight 174928bb6b v1.9.0 : modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC
Miroir fidèle du Kotlin (doc Android §7.12) :
- estrannaise-models.js : forme close 3C de estrannaise.js (paramètres
  publiés EV/EU/EEn/EC/EB/EUCS), posterior MCMC (313 échantillons/ester,
  asset mcmc_samples.json ≈ 48 Ko fetché au démarrage — remplace le fetch
  des tables ODS, 550 Ko), t½ terminale analytique, mapping en objets
  {d,k1,k2,k3} (miroir du Param Kotlin).
- estrannaise-cloud.js : nuage d'incertitude — 32 courbes du posterior
  (échelonnées, déterministes) superposant les doses des porteurs ESE ;
  exclusif ESE (TFS/WHSAH sans posterior publié), gardes (sans dose,
  nbCurves<2, MCMC non chargé → vide).
- pk-engine.js : dispatch ESE → forme close (plus de fallback PKProfileStore
  au runtime) ; cutoffHours ESE → 10 × t½ analytique ; lab-timing.js t½ ESE
  analytique ; app.js fetch MCMC remplace le fetch des tables ODS
  (démarrage plus rapide).
- chart.js/chart-canvas.js : chip « Nuage » (off, activable si ESE affiché,
  coupure auto si ESE off), clé 'CLOUD' skippée des légendes (leçon #63),
  nuage dessiné en alpha faible, légende dédiée ; seed démo + traitement
  EV/ESE (2 doses) pour démontrer le nuage ; choicesForModel ESE=6.
- Tests : estrannaise-models.test.js (6 : fidélité RMS/pics, dégénérés,
  MCMC 313×6, t½ analytique, gardes) + estrannaise-cloud.test.js (5 :
  32 courbes à dispersion réelle, fenêtre, exclusivité ESE, gardes)
  → 166 verts + E2E (chip/légende/exclusivité nuage) + check --release.
- WEB_VERSION 1.9.0 (versions sync Android). CHANGELOG [1.9.0] web.
2026-09-19 20:45:53 +02:00

142 lines
5.9 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 **Estrannaise analytique** (v1.9.0, miroir fidèle de
* `pk/EstrannaiseModels.kt`) — remplace définitivement les tables horaires
* ODS pour le modèle ESE.
*
* SOURCE ORIGINELLE : https://github.com/WHSAH/estrannaise.js/ — le modèle 3
* compartiments avec les paramètres d'ajustement **publiés par Estrannaise**
* (t en JOURS, dose en mg → pg/mL) :
*
* C(t) = dose · d · k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3))
* − e^(−k2·t)/((k1−k2)(k2−k3))
* + e^(−k3·t)/((k1−k3)(k2−k3)) ]
*
* FIDÉLITÉ (épinglée par EstrannaiseModelsTest) : cette forme close
* reproduit les anciennes tables ODS à l'identique (RMS 0,00 sur 0→200 j,
* écart de pic ≤ 0,1 %) — l'ODS n'était que l'échantillonnage horaire de
* ces formules. Décision v1.9.0 : « tous les modèles ont maintenant leurs
* sources originelles — on abandonne complètement les liens avec le
* fichier ODS ».
*
* NUAGE D'INCERTITUDE (v1.9.0, exclusif ESE) : Estrannaise publie le
* **posterior MCMC** de ses paramètres (313 échantillons par ester —
* `assets/mcmc_samples.json`, 48 Ko, fetché au démarrage). Le graphique
* peut dessiner un nuage diffus de courbes montrant la plage
* d'imprécision du modèle. TFS/WHSAH ne publient pas de posterior.
*
* PUR sauf [init] (fetch de l'asset MCMC) — la formule est testable en
* Node (tests/estrannaise-models.test.js).
* ─────────────────────────────────────────────────────────────────────────────
*/
// ── Paramètres d'ajustement publiés par Estrannaise (src/modeldata.js) ──────
// Clés = clés esters de l'app. Mapping depuis le repo : « EV im »→EV,
// « EUn im »→EU, « EEn im »→EEn, « EC im »→EC, « EB im »→EB,
// « EUn casubq »→EUCS (nouveau code ester v1.9.0).
const PK_PARAMETERS = {
EV: [478.0, 0.236, 4.85, 1.24],
EU: [471.5, 0.01729, 6.528, 2.285],
EEN: [191.4, 0.119, 0.601, 0.402],
EC: [246.0, 0.0825, 3.57, 0.669],
EB: [1893.1, 0.67, 61.5, 4.34],
EUCS: [16.15, 0.046, 0.022, 0.101],
};
let mcmc = null; // posterior MCMC (chargé via init()) — { ester: [[d,k1,k2,k3] × 313] }
/**
* Fetch de l'asset `assets/mcmc_samples.json` (48 Ko) puis init — appelé au
* démarrage par app.js (remplace l'ancien fetch des tables ODS, 550 Ko).
* Silencieux en cas d'échec (le nuage restera simplement vide — les courbes
* nommées ne dépendent pas du posterior).
*/
export async function init() {
try {
const resp = await fetch('assets/mcmc_samples.json');
if (resp.ok) initWithJson(await resp.text());
} catch (_e) {
// best-effort : sans MCMC, pas de nuage — les courbes restent calculables
}
}
export function hasModel(ester) {
return Object.prototype.hasOwnProperty.call(PK_PARAMETERS, String(ester).toUpperCase());
}
/** Les paramètres de l'ester, ou null (lookup insensible à la casse). */
export function model(ester) {
const key = String(ester).toUpperCase();
if (!hasModel(key)) return null;
const [d, k1, k2, k3] = PK_PARAMETERS[key];
return {
d, k1, k2, k3,
/** t½ terminale ANALYTIQUE : la phase la plus lente domine la queue. */
get terminalHalfLifeDays() {
return Math.log(2) / Math.min(k1, k2, k3);
},
};
}
/** Forme close 3C (t en JOURS) — cas général + 3 cas dégénérés (taux égaux). */
function curveDays(t, dose, m) {
if (t < 0) return 0.0;
let ret = 0.0;
if (dose > 0 && m.d > 0) {
const { k1, k2, k3 } = m;
if (k1 === k2 && k2 === k3) {
ret += dose * m.d * k1 * k1 * t * t * Math.exp(-k1 * t) / 2;
} else if (k1 === k2 && k2 !== k3) {
ret += dose * m.d * k1 * k1 * (Math.exp(-k3 * t) - Math.exp(-k1 * t) * (1 + (k1 - k3) * t)) / (k1 - k3) / (k1 - k3);
} else if (k1 !== k2 && k1 === k3) {
ret += dose * m.d * k1 * k2 * (Math.exp(-k2 * t) - Math.exp(-k1 * t) * (1 + (k1 - k2) * t)) / (k1 - k2) / (k1 - k2);
} else if (k1 !== k2 && k2 === k3) {
ret += dose * m.d * k1 * k2 * (Math.exp(-k1 * t) - Math.exp(-k2 * t) * (1 - (k1 - k2) * t)) / (k1 - k2) / (k1 - k2);
} else {
ret += dose * m.d * k1 * k2 * (
Math.exp(-k1 * t) / (k1 - k2) / (k1 - k3)
- Math.exp(-k2 * t) / (k1 - k2) / (k2 - k3)
+ Math.exp(-k3 * t) / (k1 - k3) / (k2 - k3)
);
}
}
return Number.isNaN(ret) ? 0.0 : ret;
}
/**
* Réponse normalisée (pg/mL PAR mg) à dtHours — t converti en jours ici.
* dtHours ≤ 0 → 0.
*/
export function sample(ester, dtHours) {
const m = model(ester);
if (!m || dtHours <= 0) return 0.0;
return curveDays(dtHours / 24, 1.0, m);
}
/** Courbe avec un jeu de paramètres DONNÉ (échantillon MCMC) — nuage. */
export function sampleWithParams(ester, dtHours, p) {
if (dtHours <= 0) return 0.0;
return curveDays(dtHours / 24, 1.0, p);
}
/**
* Charge le posterior MCMC depuis `assets/mcmc_samples.json` (appelé au
* démarrage par app.js — remplace le fetch des tables ODS).
* @param {string} json contenu JSON brut
*/
export function initWithJson(json) {
const raw = JSON.parse(json);
// Mapper vers des objets {d, k1, k2, k3} (miroir du Param Kotlin) — les
// tableaux bruts du JSON ne portent pas les noms de champs
const parsed = {};
for (const [ester, arr] of Object.entries(raw)) {
parsed[ester.toUpperCase()] = arr.map(([d, k1, k2, k3]) => ({ d, k1, k2, k3 }));
}
mcmc = parsed;
}
/** Les échantillons MCMC d'un ester (vide si non chargé / inconnu). */
export function mcmcSamples(ester) {
return (mcmc && mcmc[String(ester).toUpperCase()]) || [];
}