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.
104 lines
4.4 KiB
JavaScript
104 lines
4.4 KiB
JavaScript
/**
|
||
* NUAGE D'INCERTITUDE (v1.9.0, miroir fidèle de `pk/EstrannaiseCloud.kt`) —
|
||
* feature **exclusive au modèle ESE**.
|
||
*
|
||
* SOURCE : Estrannaise publie le **posterior MCMC** de ses paramètres
|
||
* (313 échantillons `(d, k1, k2, k3)` par ester injectable — cf
|
||
* js/pk/estrannaise-models.js). Le graphique dessine un **nuage diffus** :
|
||
* M courbes (une par échantillon échelonné) qui montrent la plage
|
||
* d'imprécision possible du modèle — l'affichage du site estrannaise.
|
||
* TFS/WHSAH ne publient pas de posterior → pas de nuage (choix confirmé).
|
||
*
|
||
* QU'EST-CE QUE LE NUAGE REPRÉSENTE : la superposition des doses des
|
||
* traitements E2 **à profil ESE** évaluée avec UN échantillon MCMC par
|
||
* courbe. Les contributions des autres traitements ne sont PAS incluses
|
||
* (pas d'incertitude publiée pour eux — limite documentée §12).
|
||
*
|
||
* DÉTERMINISME : échantillons **échelonnés** (stride, pas d'aléatoire) —
|
||
* le nuage est identique à chaque affichage et testable en Node.
|
||
*
|
||
* PUR (aucun DOM) → testable en Node (tests/estrannaise-cloud.test.js).
|
||
*/
|
||
|
||
import * as EstrannaiseModels from './estrannaise-models.js';
|
||
import { usesProfileModel } from './pk-engine.js';
|
||
|
||
|
||
/** Nombre de courbes du nuage (compromis étendue visuelle / coût ×32). */
|
||
export const DEFAULT_NB_CURVES = 32;
|
||
|
||
/** Un creux à moins de 6 h n'est pas exploitable (réutilisé pour l'horizon). */
|
||
export const MIN_HORIZON_HOURS = 6;
|
||
|
||
/**
|
||
* Calcule le nuage d'incertitude.
|
||
*
|
||
* @param {object[]} treatments TOUS les traitements (actifs ET inactifs, §6.bis)
|
||
* @param {object[]} doseLogs toutes les doses réelles
|
||
* @param {number} startMs fenêtre demandée (celle du graphique)
|
||
* @param {number} endMs
|
||
* @param {number} stepMs pas de la grille (stepForRange)
|
||
* @param {number} nowMs « maintenant »
|
||
* @param {number} [nbCurves=32]
|
||
* @returns {Array<Array<{timestamp:number,e2:number,t:number}>>} M courbes —
|
||
* **vide** si rien n'est calculable : aucun traitement E2 actif à profil
|
||
* ESE couvert, aucune dose, ou MCMC non chargé.
|
||
*/
|
||
export function compute(
|
||
treatments, doseLogs, startMs, endMs, stepMs, nowMs, nbCurves = DEFAULT_NB_CURVES,
|
||
) {
|
||
if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return [];
|
||
|
||
// Porteurs : traitements E2 actifs à profil ESE avec ester couvert
|
||
const carriers = treatments.filter((t) =>
|
||
t.type === 'ESTRADIOL'
|
||
&& t.isActive
|
||
&& usesProfileModel(t)
|
||
&& t.pkModel === 'ESE'
|
||
&& EstrannaiseModels.hasModel(t.esterType));
|
||
if (carriers.length === 0) return [];
|
||
|
||
const carrierIds = new Set(carriers.map((t) => t.id));
|
||
const carrierDoses = doseLogs.filter((d) => carrierIds.has(d.treatmentId) && d.timestamp <= endMs);
|
||
if (carrierDoses.length === 0) return [];
|
||
|
||
// Échantillons échelonnés du posterior (déterministes)
|
||
const samplesByEster = new Map(
|
||
carriers.map((t) => [t.esterType, EstrannaiseModels.mcmcSamples(t.esterType)]),
|
||
);
|
||
if ([...samplesByEster.values()].some((s) => s.length === 0)) return [];
|
||
const maxSamples = Math.min(...[...samplesByEster.values()].map((s) => s.length));
|
||
const stride = Math.max(1, Math.trunc(maxSamples / Math.min(nbCurves, maxSamples)));
|
||
|
||
// Grille : de max(start, 1ʳᵉ dose du porteur) à endMs — rien avant la 1ʳᵉ dose
|
||
const firstDoseMs = Math.min(...carrierDoses.map((d) => d.timestamp));
|
||
const t0 = Math.max(startMs, firstDoseMs);
|
||
if (t0 > endMs) return [];
|
||
|
||
// ── M courbes ──────────────────────────────────────────────────────────────
|
||
const cloud = [];
|
||
for (let m = 0; m < nbCurves; m++) {
|
||
const sampleIdx = Math.min(m * stride, maxSamples - 1);
|
||
const paramsByEster = new Map(
|
||
[...samplesByEster.entries()].map(([ester, samples]) => [
|
||
ester, samples[Math.min(sampleIdx, samples.length - 1)],
|
||
]),
|
||
);
|
||
const curve = [];
|
||
for (let t = t0; t <= endMs; t += stepMs) {
|
||
let e2 = 0.0;
|
||
for (const dose of carrierDoses) {
|
||
if (dose.timestamp > t) continue;
|
||
const dtH = (t - dose.timestamp) / 3600000;
|
||
const tr = carriers.find((x) => x.id === dose.treatmentId);
|
||
e2 += EstrannaiseModels.sampleWithParams(
|
||
tr.esterType, dtH, paramsByEster.get(tr.esterType),
|
||
) * dose.doseAmount;
|
||
}
|
||
curve.push({ timestamp: t, e2, t: 0.0 });
|
||
}
|
||
if (curve.length >= 2) cloud.push(curve);
|
||
}
|
||
return cloud;
|
||
}
|