From 96a0dbc56a9629a49ce8f9491fbda82aec35450a Mon Sep 17 00:00:00 2001 From: Siphonight Date: Sat, 19 Sep 2026 21:13:17 +0200 Subject: [PATCH] =?UTF-8?q?v1.9.1=20:=20fix=20nuage=20calibr=C3=A9=20(il?= =?UTF-8?q?=20entoure=20la=20courbe=20ESE=20calibr=C3=A9e)=20+=20doc=20+4?= =?UTF-8?q?=20pg/mL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- README.md | 4 +-- app/build.gradle.kts | 4 +-- .../com/hormonetrack/pk/EstrannaiseCloud.kt | 19 ++++++++++++-- .../hormonetrack/ui/screens/ChartScreen.kt | 5 +++- .../hormonetrack/pk/EstrannaiseCloudTest.kt | 26 +++++++++++++++++++ .../pk/PharmacokineticEngineTest.kt | 15 ++++------- docs/CHANGELOG.md | 26 +++++++++++++++++++ docs/DEVELOPPEMENT.md | 1 + 8 files changed, 83 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 78b6dc2..4a04187 100644 --- a/README.md +++ b/README.md @@ -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 && 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 ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5855da4..a5e40a8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt b/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt index 1d78eb5..43f0a9b 100644 --- a/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt +++ b/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt @@ -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? = null ): List> { 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 diff --git a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt index ba67184..09d59d9 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -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)) diff --git a/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt b/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt index 883c3c5..bfc1748 100644 --- a/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt @@ -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 + ) + } } diff --git a/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt b/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt index 8331bb5..bf79f82 100644 --- a/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt @@ -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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ce46a84..61cf230 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 1da7760..5ae1fd5 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -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). |