v1.9.1 : fix nuage calibré (il entoure la courbe ESE calibrée) + doc +4 pg/mL

- EstrannaiseCloud.compute : paramètre scalePerEster — le nuage reçoit la
  MÊME calibration que la courbe ESE (autoByModel ESE). Avant : courbe ESE
  calibrée mais nuage BRUT → les deux flottaient à des échelles différentes
  (remontée : « le nuage ne s'active que autour du tracé, pas autour du
  modèle Estrannaise »). ChartScreen passe autoM?.esterScales.
- +1 test : nuage calibré ×2 = nuage brut ×2 (le nuage suit la calibration).
- Validé émulateur §16.ter (release, seed réel + traitement EV ESE actif
  de test) : nuage rendu autour de la courbe ESE calibrée, 0 crash.
- CHANGELOG 1.9.1 : explication documentée du +4 pg/mL constaté depuis
  v1.8.2 (l'arrondi de la médiane de calibration — plus juste que la
  troncature — a déplacé une échelle auto-calibrée d'un cran ; le niveau
  actuel est le CORRIGÉ). §2 session, §7.12 nuage calibré, README.
- DIAG du test médiane nettoyé (l'épinglage de l'arrondi reste, en
  assertion propre ±0,005).
- versionCode 39 / versionName 1.9.1. Web sync v1.9.1.
This commit is contained in:
Siphonight 2026-09-19 21:13:17 +02:00
parent 8078d5cbed
commit 77ca672078
8 changed files with 83 additions and 17 deletions

View File

@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue.
- **Statut** : v1.9.0 — build Android ✅, **lint vert** ✅, **234 tests unitaires** ✅ (204 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Statut** : v1.9.1 — build Android ✅, **lint vert** ✅, **235 tests unitaires** ✅ (205 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -123,7 +123,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
git clone <repo> && cd HormoneTrack
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 234 tests (204 sans les données locales)
./gradlew testDebugUnitTest # 235 tests (205 sans les données locales)
./gradlew lint # lint vert obligatoire avant release
```

View File

@ -17,8 +17,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 38
versionName = "1.9.0"
versionCode = 39
versionName = "1.9.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -64,7 +64,17 @@ object EstrannaiseCloud {
endMs: Long,
stepMs: Long,
nowMs: Long,
nbCurves: Int = DEFAULT_NB_CURVES
nbCurves: Int = DEFAULT_NB_CURVES,
/**
* Calibration à appliquer au nuage (v1.9.1 — fix « le nuage ne
* s'active que autour du tracé émulé, pas autour du modèle
* Estrannaise ») : la courbe ESE affichée est CALIBRÉE
* (scalePerEster des labs) mais le nuage était tracé BRUT → les deux
* flottaient à des échelles différentes. Passer le MÊME
* `scalePerEster` que la courbe ESE (autoByModel["ESE"]?.esterScales
* côté ChartScreen) pour que le nuage ENTOURE la courbe.
*/
scalePerEster: Map<String, Double>? = null
): List<List<LevelPoint>> {
if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return emptyList()
@ -115,7 +125,12 @@ object EstrannaiseCloud {
val dtH = (t - dose.timestamp) / 3_600_000.0
val tr = carriers.first { it.id == dose.treatmentId }
val params = paramsByEster.getValue(tr.esterType)
e2 += EstrannaiseModels.sampleWithParams(tr.esterType, dtH, params) * dose.doseAmount
// Même calibration que la courbe ESE : le nuage ENTOURE
// la courbe calibrée (fix v1.9.1)
val scale = scalePerEster?.get(
PharmacokineticEngine.doseEster(tr, dose)
) ?: tr.scaleFactor
e2 += EstrannaiseModels.sampleWithParams(tr.esterType, dtH, params) * dose.doseAmount * scale
}
curve.add(LevelPoint(t, e2, 0.0))
t += stepMs

View File

@ -366,8 +366,11 @@ fun ChartScreen() {
// ESE (exclusif à ce modèle — les fits TFS/WHSAH ne
// publient pas de posterior MCMC)
val cloud = if (model == "ESE" && showCloud) {
// MÊME calibration que la courbe ESE (fix v1.9.1 :
// nuage brut ≠ échelle de la courbe calibrée)
EstrannaiseCloud.compute(
treatments, doseLogs, startMs, endMs, step, nowMs
treatments, doseLogs, startMs, endMs, step, nowMs,
scalePerEster = autoM?.esterScales
)
} else emptyList()
list.add(ChartCurve(model, curve, cloud))

View File

@ -106,4 +106,30 @@ class EstrannaiseCloudTest {
listOf(ese()), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW, nbCurves = 1
).isEmpty())
}
@Test
fun `cloud follows the calibration like the displayed ESE curve (fix v1_9_1)`() {
// FIX « le nuage ne s'active que autour du tracé, pas autour du
// modèle Estrannaise » : la courbe ESE affichée est CALIBRÉE
// (scalePerEster des labs) mais le nuage était tracé BRUT → les deux
// flottaient à des échelles différentes. Avec le MÊME scalePerEster,
// le nuage ENTOURE la courbe calibrée (×2 ⇒ nuage ×2, écart de
// niveau entre nuage calibré et nuage brut ≈ ×2 également).
val scaled = mapOf("EEN" to 2.0)
val calibrated = EstrannaiseCloud.compute(
listOf(ese()), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW,
scalePerEster = scaled
)
val raw = EstrannaiseCloud.compute(
listOf(ese()), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW
)
assertEquals(calibrated.size, raw.size)
assertTrue(calibrated.isNotEmpty())
// Chaque point du nuage calibré ≈ 2× le point brut correspondant
val rawLast = raw.last()
val calLast = calibrated.last()
assertEquals(
calLast.last().e2, rawLast.last().e2 * 2.0, rawLast.last().e2 * 0.01
)
}
}

View File

@ -160,16 +160,11 @@ class PharmacokineticEngineTest {
LabResult(marker = "E2", value = predicted * r, unit = "pg/mL", timestamp = t)
}
val sf = PharmacokineticEngine.computeScaleFactor(tr, doses, labs)
// DIAG v1.8.2 : comparaison e2At (chemin production) vs Σ
// concentrationOfDose (chemin computeScaleFactor)
val ratiosDiag = labs.sortedBy { it.timestamp }.map { lab ->
val viaE2At = PharmacokineticEngine.e2At(listOf(tr), doses, lab.timestamp)
val viaDirect = doses.filter { it.timestamp <= lab.timestamp }.sumOf { d ->
PharmacokineticEngine.concentrationOfDose(tr, d, lab.timestamp)
}
"e2At=%.6f/direct=%.6f/ratioSF=%.6f".format(viaE2At, viaDirect, lab.value / viaDirect)
}
assertEquals("ratios=$ratiosDiag sf=$sf", 0.9, sf!!, 0.01)
// v1.8.2 : la médiane est ARRONDIE (Math.round), pas tronquée — le
// dernier bit flottant (0,8999… vs 0,9000…) ne doit pas faire chuter
// l'échelle d'un cran d'affichage (révélé par le passage d'ESE en
// analytique v1.9.0, cf CHANGELOG 1.9.1)
assertEquals(0.9, sf!!, 0.005)
}
@Test

View File

@ -3,6 +3,32 @@
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
## [1.9.1] — 2026-09-19 (versionCode 39)
### Corrigé — le nuage d'incertitude entoure maintenant la courbe Estrannaise calibrée
- **Bug remonté** : le nuage ne s'activait « que autour du tracé, pas autour
du modèle Estrannaise » — la courbe ESE affichée est **calibrée**
(scalePerEster des labs) mais le nuage était tracé **brut** : les deux
flottaient à des échelles différentes.
- **Fix** : le nuage reçoit le MÊME `scalePerEster` que la courbe ESE
(autoByModel ESE) — il entoure désormais la courbe calibrée (Android +
web en miroir).
- 1 nouveau test : nuage calibré ×2 = nuage brut ×2 (le nuage suit la
calibration).
### Note — pourquoi le taux affiché a bougé depuis v1.8.2 (+4 pg/mL)
- v1.8.2 a corrigé l'**arrondi de la médiane de calibration** (troncature
flottante → arrondi au plus proche) : une échelle dont la médiane brute
tombait à 0,8999… était tronquée en 0,89 au lieu de 0,90. Avec le modèle
ESE analytique (v1.9.0) déplaçant ce dernier bit, le piège est devenu
visible : les échelles auto-calibrées ont bougé d'un cran d'affichage →
le niveau estimé a monté d'environ 1 % (+4 pg/mL sur ~320).
- **Le niveau actuel est le CORRIGÉ** (l'arrondi est plus juste que la
troncature). Le web applique la même règle depuis v1.9.1 (harmonisation
des niveaux entre plateformes).
## [1.9.0] — 2026-09-18 (versionCode 38)
### Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC

View File

@ -63,6 +63,7 @@ Fonctionnalités v1 :
| Date | Événement |
|---|---|
| 19 sept. 2026 (session v1.9.1) | **Fix nuage calibré + suggestion sur l'Accueil + explication +4 pg/mL** : (a) remontée « le nuage ne s'active que autour du tracé, pas autour du modèle Estrannaise » : la courbe ESE est CALIBRÉE (scalePerEster) mais le nuage était BRUT → échelles différentes. FIX : EstrannaiseCloud.compute reçoit le MÊME scalePerEster que la courbe ESE (Android + web miroir) ; test nuage calibré ×2 (Android). (b) Le +4 pg/mL constaté depuis v1.8.2 : l'ARRONDI de la médiane (fix v1.8.2, plus juste que la troncature) a déplacé une échelle auto-calibrée d'un cran — le niveau actuel est le CORRIGÉ, documenté CHANGELOG 1.9.1. (c) Émulateur : nuage visible autour de la courbe ESE calibrée (1363 px rose pâle, seed réel + traitement EV ESE de test), 0 crash. Web v1.9.1 sync (harmonisation de l'arrondi — les niveaux web/android convergent). 235 verts / 205 sans données locales. |
| 17 sept. 2026 (session v1.9.0) | **Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC (Android + web sync)** (demande) : découverte des sources originelles (github.com/WHSAH/estrannaise.js — forme close 3C + paramètres publiés + posterior MCMC 313/ester). Vérification numérique AVANT débranchement : **RMS 0,00 vs tables ODS** (l'ODS était l'échantillonnage de ces formules). ESE → analytique (dispatch, cutoff 10×t½, t½ LabTiming), PKProfileStore retiré du runtime (asset ODS → src/test/assets, −550 Ko d'APK), ESE couvre 6 esters (+EUCS), TFS = liste explicite (EUCS sans V3C TFS). **Nuage MCMC exclusif ESE** : chip `Nuage` (off, activable si ESE affiché, coupure auto si ESE off), 32 courbes du posterior déterministes, alpha faible sous les courbes, hors échelles/labels/extrema. 11 nouveaux tests (234 verts / 204 sans données locales) + lint + check web (166). Validé émulateur §16.ter : courbe ESE analytique affichée, unités, chips Extend/Cloud, 0 crash. |
| 17 sept. 2026 (session v1.8.2) | **Maintenance — audit complet code + doc** : (a) **perf moteur** : cutoffHours pré-calculé par traitement dans e2At/computeCurve (le chemin chaud du tracé labs recalculait ~430 k appels/refresh), Bateman paresseux (inutile pour les traitements à profil), doses pré-groupées par traitement — résultat identique (ordre de sommation préservé, 223 tests verts inchangés) ; (b) **2 fixes de fraîcheur** (#65 accueil : clés produceState sans allTreatments/tConfig — éditer un scaleFactor n'actualisait pas la courbe avant le tick ; #66 graphiques : clé `labResults.size` — éditer la valeur d'un lab ne recalculait pas) ; (c) **nettoyage** : ~9 imports morts, qualifications raccourcies, code mort (Repository/DAO, currentLevel, scheduler2), médiane factorisée (4 copies), SimpleDateFormat hors boucle, AppLog sans recopie, Regex précompilées, formatDose dédupliqué ; (d) **doc** : ~35 corrections (compteurs actuels 223/193 partout, TOC + ancres réparées, inventaire §8 à jour et fusionné, doublons CHANGELOG, §19 backup, fautes). Audit préalable par double exploration lecture-seule (46 fichiers + 4 docs). 223 verts + lint + émulateur smoke. Android seul (web non concerné — re-render complet, aucune release web). |
| 17 sept. 2026 (session v1.8.1) | **Fix stabilisation + suggestion sur l'Accueil** (critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app me disait stabilisée depuis février ») : le proxy « 1ʳᵉ dose du traitement » était aveugle aux changements récents. NOUVELLE règle `regimeStartMs` : le régime courant = la **séquence terminale de doses** à (ester effectif, dose mg, **écart inter-doses**) constants (l'écart comparé EXACTEMENT — 7 j ± 1 h casse le régime) ; tout changement récent réinitialise la stabilisation → la reco saute au premier creux post-stabilisation ; l'ester EFFECTIF de la dernière dose (override compris) alimente la carte. **+ Suggestion aussi sur l'Accueil** (demande) : carte compacte (creux daté + créneau, mention de stabilisation seulement si le régime n'était pas déjà stable). 3 nouveaux tests (223 verts / 193 sans données locales). Validé émulateur (release, seed réel) : la carte passe de « stabilisé depuis le 06/02 » à « EEN pas stabilisé avant le 29/09 — premier creux fiable », creux 27/09 22:43, 0 crash. Web v1.8.1 sync (3 tests miroirs + encart Accueil). |