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.
142 lines
5.9 KiB
JavaScript
142 lines
5.9 KiB
JavaScript
/**
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
* 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()]) || [];
|
||
}
|