v1.4.8 : calibration PAR MODÈLE PK (#60) — le graphique calcule une AutoCalibrated par modèle affiché (modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs (plus de facteur partagé absurde ×2,21 sur WHSAH) ; Home inchangé (modèle stocké) ; CalibrationPerModelTest (5 tests : couverture 3 modèles, délégation unique, k T par modèle, cas dégénérés) ; 164 tests verts + lint
This commit is contained in:
parent
1c9d2e4e6a
commit
b45b6affb2
61
README.md
61
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
|
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
|
||||||
> sang et aux consignes de ton endocrinologue.
|
> sang et aux consignes de ton endocrinologue.
|
||||||
|
|
||||||
- **Statut** : v1.4.7 — build Android ✅, **lint vert** ✅, **158 tests unitaires** ✅ (136 sans les données de test locales ; 3 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.4.8 — build Android ✅, **lint vert** ✅, **159 tests unitaires** ✅ (137 sans les données de test locales ; 3 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)
|
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.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)
|
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
|
||||||
@ -37,7 +37,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
[article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)),
|
[article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)),
|
||||||
avec les **7 esters** (EV, EU, EEn, EB, EC huile, EC suspension, PEP) — et WHSAH
|
avec les **7 esters** (EV, EU, EEn, EB, EC huile, EC suspension, PEP) — et WHSAH
|
||||||
en couvre 6 (sans PEP). Affichés côte à côte avec toggles indépendants (3 couleurs) — **pré-cochés
|
en couvre 6 (sans PEP). Affichés côte à côte avec toggles indépendants (3 couleurs) — **pré-cochés
|
||||||
selon les modèles de tes traitements** (v1.4.7) et **tous calibrés** par tes labs
|
selon les modèles de tes traitements** (v1.4.7) et **chaque modèle calibré
|
||||||
|
séparément** par tes labs (v1.4.8 — les facteurs s'adaptent à chaque profil)
|
||||||
- **Modèle Bateman** paramétrable (temps au pic, demi-vie, biodisponibilité) pour gel,
|
- **Modèle Bateman** paramétrable (temps au pic, demi-vie, biodisponibilité) pour gel,
|
||||||
patch et voie orale
|
patch et voie orale
|
||||||
- **Simulation prévisionnelle** : configurer la **posologie** (intervalle en jours) sur un
|
- **Simulation prévisionnelle** : configurer la **posologie** (intervalle en jours) sur un
|
||||||
@ -92,7 +93,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
|
|||||||
git clone <repo> && cd HormoneTrack
|
git clone <repo> && cd HormoneTrack
|
||||||
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
|
||||||
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
|
||||||
./gradlew testDebugUnitTest # 158 tests (136 sans les données locales)
|
./gradlew testDebugUnitTest # 159 tests (137 sans les données locales)
|
||||||
./gradlew lint # lint vert obligatoire avant release
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS
|
|||||||
|
|
||||||
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) —
|
||||||
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
`Siphonight/HormoneTrack` sur les deux (privé), avec
|
||||||
**releases taguées** (`v1.1.0` → `v1.4.7`) et **deux APK par release** (depuis v1.2.5)
|
**releases taguées** (`v1.1.0` → `v1.4.8`) et **deux APK par release** (depuis v1.2.5)
|
||||||
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
(téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) :
|
||||||
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
`-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) :
|
||||||
|
|
||||||
@ -120,25 +121,45 @@ taggué annoté. Voir [docs/DEVELOPPEMENT.md §16](docs/DEVELOPPEMENT.md).
|
|||||||
## Les modèles en bref
|
## Les modèles en bref
|
||||||
|
|
||||||
Chaque injection contribue `dose_mg × profil(dt)` où `profil` est la réponse normalisée
|
Chaque injection contribue `dose_mg × profil(dt)` où `profil` est la réponse normalisée
|
||||||
(pg/mL par mg) ; les contributions se superposent. **Estrannaise** = tables horaires
|
(pg/mL par mg) ; les contributions se superposent. **Trois modèles superposables** :
|
||||||
du `.ods` (8001 h) ; **Transfem Science** = méta-analyse à 3 compartiments (V3C,
|
|
||||||
forme close, [article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)).
|
- **Estrannaise** = tables horaires du `.ods` (8001 h), EV/EU/EEn
|
||||||
|
- **Transfem Science** = méta-analyse à 3 compartiments (V3C, forme close,
|
||||||
|
[article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)),
|
||||||
|
7 esters (EV, EU, EEn, EB, EC, EC suspension, PEP)
|
||||||
|
- **WHSAH** (v1.4.6) = fit « license-free » du [WHSAH Collective via
|
||||||
|
Mona](https://github.com/mona-hrt/mona) — même famille mathématique mais
|
||||||
|
paramètres indépendants avec biodisponibilité explicite F < 1 : montée
|
||||||
|
plus rapide à J+1 (EEn ~70 pg/mL à J+1 pour 5 mg vs ~22 chez TFS) et
|
||||||
|
décroissance plus longue (t½ EEn 7,3 j vs 4,5 j). 6 esters (sans PEP)
|
||||||
|
|
||||||
Pics de référence (pg/mL par mg) :
|
Pics de référence (pg/mL par mg) :
|
||||||
|
|
||||||
| Profil | Modèle | Pic (pg/mL/mg) | Tmax |
|
| Profil | Modèle | Pic (pg/mL/mg) | Tmax | t½ term. |
|
||||||
|----------|------------------|----------------|--------|
|
|----------|------------------|----------------|--------|----------|
|
||||||
| EV | Estrannaise | 61,1 | ~45 h |
|
| EV | Estrannaise | 61,1 | ~45 h | — |
|
||||||
| EU | Estrannaise | 3,4 | ~55 h (plateau long) |
|
| EU | Estrannaise | 3,4 | ~55 h (plateau long) | — |
|
||||||
| EEn | Estrannaise | 31,4 | ~152 h |
|
| EEn | Estrannaise | 31,4 | ~152 h | — |
|
||||||
| EV | Transfem Science | 59,0 | ~51 h |
|
| EV | Transfem Science | 59,0 | ~51 h | 3,0 j |
|
||||||
| EU | Transfem Science | 10,1 | ~198 h |
|
| EU | Transfem Science | 10,1 | ~198 h | — |
|
||||||
| EEn | Transfem Science | 32,0 | ~156 h |
|
| EEn | Transfem Science | 32,0 | ~156 h | 4,5 j |
|
||||||
| EB | Transfem Science | 194,2 | ~16 h |
|
| EB | Transfem Science | 194,2 | ~16 h | 1,2 j |
|
||||||
| EC (huile) | Transfem Science | 31,1 | ~103 h |
|
| EC (huile) | Transfem Science | 31,1 | ~103 h | 6,7 j |
|
||||||
| PEP | Transfem Science | 1,03 (dose ~6,5×) | ~18 j |
|
| EC (susp.) | Transfem Science | 48,2 | ~29 h | 5,1 j |
|
||||||
|
| PEP | Transfem Science | 1,03 (dose ~6,5×) | ~18 j | 28,4 j |
|
||||||
|
| EV | WHSAH | 73,5 | ~41 h | 3,1 j |
|
||||||
|
| EEn | WHSAH | 37,6 | ~120 h | 7,3 j |
|
||||||
|
| EB | WHSAH | 260,1 | ~12 h | 1,3 j |
|
||||||
|
| EC (huile) | WHSAH | 25,0 | ~81 h | 7,9 j |
|
||||||
|
| EC (susp.) | WHSAH | 53,5 | ~16 h | 7,1 j |
|
||||||
|
| EU | WHSAH | 4,9 | ~67 h | 31,7 j |
|
||||||
|
|
||||||
La calibration (facteur d'échelle par traitement, calibré par tes labs) ajuste le modèle
|
**Calibration (v1.4.8)** : le facteur d'échelle s'applique par ESTER et par
|
||||||
à ton corps, exactement comme la colonne « Scale factor » de la feuille d'origine.
|
PÉRIODE d'injection (médiane des ratios lab ÷ prédiction, comme la colonne
|
||||||
|
« Scale factor » de la feuille d'origine) — et **par MODÈLE** : chaque courbe
|
||||||
|
affichée (Estrannaise / Transfem Science / WHSAH) est calibrée avec la
|
||||||
|
prédiction de SON modèle → toutes collent à tes labs, quelle que soit leur
|
||||||
|
forme. L'auto-calibration est optionnelle (désactivée par défaut).
|
||||||
|
|
||||||
## Vie privée
|
## Vie privée
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,8 @@ android {
|
|||||||
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
|
||||||
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
|
||||||
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
// les APK publiés (Paramètres + titre du dialog « Nouveautés »).
|
||||||
versionCode = 27
|
versionCode = 28
|
||||||
versionName = "1.4.7"
|
versionName = "1.4.8"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -397,7 +397,8 @@ object PharmacokineticEngine {
|
|||||||
fun computeEsterScaleFactors(
|
fun computeEsterScaleFactors(
|
||||||
treatments: List<Treatment>,
|
treatments: List<Treatment>,
|
||||||
doseLogs: List<DoseLog>,
|
doseLogs: List<DoseLog>,
|
||||||
e2Labs: List<LabResult>
|
e2Labs: List<LabResult>,
|
||||||
|
modelOverride: String? = null
|
||||||
): Map<String, Double> {
|
): Map<String, Double> {
|
||||||
val estrogenTreatments = treatments.filter { it.type == TreatmentType.ESTRADIOL }
|
val estrogenTreatments = treatments.filter { it.type == TreatmentType.ESTRADIOL }
|
||||||
val estrogenDoses = doseLogs.filter { d ->
|
val estrogenDoses = doseLogs.filter { d ->
|
||||||
@ -405,11 +406,15 @@ object PharmacokineticEngine {
|
|||||||
}
|
}
|
||||||
if (estrogenDoses.isEmpty()) return emptyMap()
|
if (estrogenDoses.isEmpty()) return emptyMap()
|
||||||
|
|
||||||
// Prédictions non calibrées (scaleFactor forcé à 1) pour chaque lab
|
// Prédictions non calibrées (scaleFactor forcé à 1) pour chaque lab,
|
||||||
|
// avec le MODÈLE demandé (fix #60 : chaque courbe est calibrée avec
|
||||||
|
// SA propre prédiction — v1.4.7 calculait tout depuis le modèle
|
||||||
|
// stocké du traitement, ce qui produisait des facteurs absurdes dès
|
||||||
|
// qu'un traitement basculait de modèle, ex. ×2,21 sur WHSAH)
|
||||||
val unscaled = estrogenTreatments.map { it.copy(scaleFactor = 1.0) }
|
val unscaled = estrogenTreatments.map { it.copy(scaleFactor = 1.0) }
|
||||||
val ratiosByEster = HashMap<String, MutableList<Double>>()
|
val ratiosByEster = HashMap<String, MutableList<Double>>()
|
||||||
for (lab in e2Labs) {
|
for (lab in e2Labs) {
|
||||||
val predicted = e2At(unscaled, doseLogs, lab.timestamp)
|
val predicted = e2At(unscaled, doseLogs, lab.timestamp, modelOverride = modelOverride)
|
||||||
if (predicted <= 0.5) continue
|
if (predicted <= 0.5) continue
|
||||||
|
|
||||||
// Attribution : dernière dose E2 ≤ lab → son ester (override compris)
|
// Attribution : dernière dose E2 ≤ lab → son ester (override compris)
|
||||||
@ -451,7 +456,8 @@ object PharmacokineticEngine {
|
|||||||
doseLogs: List<DoseLog>,
|
doseLogs: List<DoseLog>,
|
||||||
tLabs: List<LabResult>,
|
tLabs: List<LabResult>,
|
||||||
current: TConfig,
|
current: TConfig,
|
||||||
scalePerEster: Map<String, Double>? = null
|
scalePerEster: Map<String, Double>? = null,
|
||||||
|
modelOverride: String? = null
|
||||||
): Map<String, Double> {
|
): Map<String, Double> {
|
||||||
val estrogenDoses = doseLogs.filter { d ->
|
val estrogenDoses = doseLogs.filter { d ->
|
||||||
treatments.any { it.id == d.treatmentId && it.type == TreatmentType.ESTRADIOL }
|
treatments.any { it.id == d.treatmentId && it.type == TreatmentType.ESTRADIOL }
|
||||||
@ -469,7 +475,9 @@ object PharmacokineticEngine {
|
|||||||
val ester = doseEster(tr, attributed)
|
val ester = doseEster(tr, attributed)
|
||||||
if (ester == Esters.NONE) continue
|
if (ester == Esters.NONE) continue
|
||||||
|
|
||||||
val e2 = e2At(treatments, doseLogs, lab.timestamp, scalePerEster = scalePerEster)
|
// v1.4.8 : l'E2 de référence suit le MODÈLE demandé (fix #60)
|
||||||
|
val e2 = e2At(treatments, doseLogs, lab.timestamp, scalePerEster = scalePerEster,
|
||||||
|
modelOverride = modelOverride)
|
||||||
if (e2 <= 1.0) continue
|
if (e2 <= 1.0) continue
|
||||||
val k = ((current.base - current.floor) / (tNgMl - current.floor) - 1.0) / e2
|
val k = ((current.base - current.floor) / (tNgMl - current.floor) - 1.0) / e2
|
||||||
if (k > 1e-4 && k < 10.0) {
|
if (k > 1e-4 && k < 10.0) {
|
||||||
@ -511,14 +519,23 @@ object PharmacokineticEngine {
|
|||||||
treatments: List<Treatment>,
|
treatments: List<Treatment>,
|
||||||
doseLogs: List<DoseLog>,
|
doseLogs: List<DoseLog>,
|
||||||
labs: List<LabResult>,
|
labs: List<LabResult>,
|
||||||
tConfig: TConfig
|
tConfig: TConfig,
|
||||||
|
/**
|
||||||
|
* Modèle PK pour les PRÉDICTIONS de calibration (v1.4.8, fix #60) :
|
||||||
|
* null = modèle stocké de chaque traitement (Home). Le graphique
|
||||||
|
* appelle une fois PAR MODÈLE affiché (ESE/TFS/WHS) — chaque courbe
|
||||||
|
* reçoit ainsi SES échelles et SON k.
|
||||||
|
*/
|
||||||
|
modelOverride: String? = null
|
||||||
): AutoCalibrated {
|
): AutoCalibrated {
|
||||||
val e2Labs = labs.filter { it.marker.equals("E2", true) }
|
val e2Labs = labs.filter { it.marker.equals("E2", true) }
|
||||||
val tLabs = labs.filter { it.marker.equals("T", true) }
|
val tLabs = labs.filter { it.marker.equals("T", true) }
|
||||||
|
|
||||||
val esterScales = computeEsterScaleFactors(treatments, doseLogs, e2Labs)
|
val esterScales = computeEsterScaleFactors(treatments, doseLogs, e2Labs,
|
||||||
|
modelOverride = modelOverride)
|
||||||
val tKPerEster = computeTKPerEster(
|
val tKPerEster = computeTKPerEster(
|
||||||
treatments, doseLogs, tLabs, tConfig, scalePerEster = esterScales
|
treatments, doseLogs, tLabs, tConfig, scalePerEster = esterScales,
|
||||||
|
modelOverride = modelOverride
|
||||||
)
|
)
|
||||||
return AutoCalibrated(
|
return AutoCalibrated(
|
||||||
treatments = treatments,
|
treatments = treatments,
|
||||||
|
|||||||
@ -146,14 +146,34 @@ fun ChartScreen() {
|
|||||||
|
|
||||||
// Auto-calibration (v1.2.1) : facteurs d'échelle PAR PÉRIODE D'ESTER + modèle T,
|
// Auto-calibration (v1.2.1) : facteurs d'échelle PAR PÉRIODE D'ESTER + modèle T,
|
||||||
// recalculés à la volée depuis les labs. null = option désactivée.
|
// recalculés à la volée depuis les labs. null = option désactivée.
|
||||||
val effectiveAuto by produceState<PharmacokineticEngine.AutoCalibrated?>(
|
// ── Calibration PAR MODÈLE (v1.4.8, fix #60) ──────────────────────────────
|
||||||
null, treatments, doseLogs, labResults, tConfig, autoCalibrate
|
// Demande : « la calibration doit être active sur tous les modèles si elle
|
||||||
|
// est activée, mais les facteurs ne doivent pas être identiques entre les
|
||||||
|
// profils PK ». AVANT : un SEUL jeu de facteurs calculé depuis le modèle
|
||||||
|
// STOCKÉ des traitements puis partagé par toutes les courbes → en
|
||||||
|
// basculant un traitement (ex. EEn TFS → WHSAH), le facteur devenait
|
||||||
|
// absurde (×2,21, remontée). DÉSORMAIS : une AutoCalibrated PAR modèle
|
||||||
|
// (ESE/TFS/WHS), chacune calibrée avec la prédiction DE SON modèle →
|
||||||
|
// chaque courbe affichée colle à ses labs. Home continue d'utiliser le
|
||||||
|
// modèle stocké (calcul sans override).
|
||||||
|
val autoByModel by produceState<Map<String, PharmacokineticEngine.AutoCalibrated>>(
|
||||||
|
emptyMap(), treatments, doseLogs, labResults, tConfig, autoCalibrate
|
||||||
) {
|
) {
|
||||||
value = if (!autoCalibrate) {
|
value = if (!autoCalibrate) {
|
||||||
null
|
emptyMap()
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
PharmacokineticEngine.autoCalibrated(treatments, doseLogs, labResults, tConfig)
|
mapOf(
|
||||||
|
"ESE" to PharmacokineticEngine.autoCalibrated(
|
||||||
|
treatments, doseLogs, labResults, tConfig, modelOverride = "ESE"
|
||||||
|
),
|
||||||
|
"TFS" to PharmacokineticEngine.autoCalibrated(
|
||||||
|
treatments, doseLogs, labResults, tConfig, modelOverride = "TFS"
|
||||||
|
),
|
||||||
|
"WHS" to PharmacokineticEngine.autoCalibrated(
|
||||||
|
treatments, doseLogs, labResults, tConfig, modelOverride = "WHS"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,16 +249,16 @@ fun ChartScreen() {
|
|||||||
// Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS")
|
// Une liste de courbes par modèle sélectionné (clé = "ESE" / "TFS")
|
||||||
val curves by produceState(
|
val curves by produceState(
|
||||||
emptyList<Pair<String, List<LevelPoint>>>(),
|
emptyList<Pair<String, List<LevelPoint>>>(),
|
||||||
effectiveAuto, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast
|
autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast
|
||||||
) {
|
) {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
||||||
if (showEse || showTfs || showWhs) {
|
if (showEse || showTfs || showWhs) {
|
||||||
// Valeurs actuelles : échelles par ester si auto-calibration active,
|
// Calibration PAR MODÈLE (fix #60) : chaque courbe reçoit les
|
||||||
// sinon le scaleFactor stocké de chaque traitement
|
// échelles/k calculés avec SA prédiction. Hors auto-calibration
|
||||||
val effTreatments = effectiveAuto?.treatments ?: treatments
|
// → scaleFactor stocké des traitements (comportement historique).
|
||||||
val effTConfig = effectiveAuto?.tConfig ?: tConfig
|
val effTreatments = autoByModel["TFS"]?.treatments ?: treatments
|
||||||
val effScales = effectiveAuto?.esterScales
|
val effTConfig = autoByModel["TFS"]?.tConfig ?: tConfig
|
||||||
// Doses réelles + doses prévisionnelles éventuelles
|
// Doses réelles + doses prévisionnelles éventuelles
|
||||||
val forecastDoses = if (showForecast) {
|
val forecastDoses = if (showForecast) {
|
||||||
effTreatments.flatMap {
|
effTreatments.flatMap {
|
||||||
@ -249,36 +269,39 @@ fun ChartScreen() {
|
|||||||
// Pas d'échantillonnage adaptatif au zoom (v1.2.9)
|
// Pas d'échantillonnage adaptatif au zoom (v1.2.9)
|
||||||
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
||||||
if (showEse) {
|
if (showEse) {
|
||||||
|
val autoM = autoByModel["ESE"]
|
||||||
list.add(
|
list.add(
|
||||||
"ESE" to PharmacokineticEngine.computeCurve(
|
"ESE" to PharmacokineticEngine.computeCurve(
|
||||||
effTreatments, allDoses, startMs, endMs,
|
effTreatments, allDoses, startMs, endMs,
|
||||||
stepMs = step,
|
stepMs = step,
|
||||||
tConfig = effTConfig, modelOverride = "ESE",
|
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "ESE",
|
||||||
scalePerEster = effScales,
|
scalePerEster = autoM?.esterScales,
|
||||||
tKPerEster = effectiveAuto?.tKPerEster
|
tKPerEster = autoM?.tKPerEster
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showTfs) {
|
if (showTfs) {
|
||||||
|
val autoM = autoByModel["TFS"]
|
||||||
list.add(
|
list.add(
|
||||||
"TFS" to PharmacokineticEngine.computeCurve(
|
"TFS" to PharmacokineticEngine.computeCurve(
|
||||||
effTreatments, allDoses, startMs, endMs,
|
effTreatments, allDoses, startMs, endMs,
|
||||||
stepMs = step,
|
stepMs = step,
|
||||||
tConfig = effTConfig, modelOverride = "TFS",
|
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "TFS",
|
||||||
scalePerEster = effScales,
|
scalePerEster = autoM?.esterScales,
|
||||||
tKPerEster = effectiveAuto?.tKPerEster
|
tKPerEster = autoM?.tKPerEster
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// v1.4.6 : 3ᵉ modèle WHSAH (même mécanique d'override)
|
// v1.4.6 : 3ᵉ modèle WHSAH (même mécanique d'override)
|
||||||
if (showWhs) {
|
if (showWhs) {
|
||||||
|
val autoM = autoByModel["WHS"]
|
||||||
list.add(
|
list.add(
|
||||||
"WHS" to PharmacokineticEngine.computeCurve(
|
"WHS" to PharmacokineticEngine.computeCurve(
|
||||||
effTreatments, allDoses, startMs, endMs,
|
effTreatments, allDoses, startMs, endMs,
|
||||||
stepMs = step,
|
stepMs = step,
|
||||||
tConfig = effTConfig, modelOverride = "WHS",
|
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "WHS",
|
||||||
scalePerEster = effScales,
|
scalePerEster = autoM?.esterScales,
|
||||||
tKPerEster = effectiveAuto?.tKPerEster
|
tKPerEster = autoM?.tKPerEster
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
191
app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt
Normal file
191
app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package com.hormonetrack.pk
|
||||||
|
|
||||||
|
import com.hormonetrack.data.model.AdministrationRoute
|
||||||
|
import com.hormonetrack.data.model.DoseLog
|
||||||
|
import com.hormonetrack.data.model.Esters
|
||||||
|
import com.hormonetrack.data.model.LabResult
|
||||||
|
import com.hormonetrack.data.model.PKModels
|
||||||
|
import com.hormonetrack.data.model.Treatment
|
||||||
|
import com.hormonetrack.data.model.TreatmentType
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process de tests **CALIBRATION** (v1.4.8, fix #60) — sémantique
|
||||||
|
* « calibration PAR MODÈLE PK » : chaque courbe affichée (ESE/TFS/WHS) est
|
||||||
|
* calibrée avec la prédiction de SON modèle, donc chaque courbe calibrée
|
||||||
|
* passe par les labs.
|
||||||
|
*
|
||||||
|
* Compléments des tests voisins (pour s'y retrouver) :
|
||||||
|
* - `WhsahEngineTest.calibration is per model` : TFS vs WHS (le cas de la
|
||||||
|
* remontée ×2,21) ;
|
||||||
|
* - `WhsahEngineTest.calibration applies to ALL model curves` : le facteur
|
||||||
|
* fourni est appliqué à toutes les courbes (application uniforme) ;
|
||||||
|
* - `PharmacokineticEngineTest` : calibration T manuelle, SF médiane.
|
||||||
|
*
|
||||||
|
* Ce fichier couvre ce qui restait sans filet :
|
||||||
|
* 1. la couverture des **3 modèles** (ESE inclus) — chaque courbe calibrée
|
||||||
|
* atterrit sur le lab ;
|
||||||
|
* 2. la **délégation** `autoCalibrated(modelOverride)` →
|
||||||
|
* `computeEsterScaleFactors(modelOverride)` (le graphique et Home
|
||||||
|
* utilisent la même fonction, pas deux implémentations) ;
|
||||||
|
* 3. le **k T par modèle** (calibré contre l'E2 calibrée de SON modèle) ;
|
||||||
|
* 4. les cas **dégénérés** (labs antérieurs à la 1ʳᵉ dose → échelles vides ;
|
||||||
|
* prédiction trop faible → lab ignoré).
|
||||||
|
*/
|
||||||
|
class CalibrationPerModelTest {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val BASE = System.currentTimeMillis()
|
||||||
|
const val DAY_MS = 86_400_000L
|
||||||
|
}
|
||||||
|
|
||||||
|
@org.junit.Before
|
||||||
|
fun loadProfiles() {
|
||||||
|
// ⚠️ le chemin ESE lit les tables ODS (asset) — sans init,
|
||||||
|
// PKProfileStore.sample retourne 0 et les labs seraient ignorés
|
||||||
|
// (prédiction ≤ 0,5) → échelles vides. Les modèles TFS/WHS sont
|
||||||
|
// purs (formes closes) et n'en dépendent pas.
|
||||||
|
if (!PKProfileStore.hasProfile("EEN", "ESE")) {
|
||||||
|
val file = java.io.File("src/main/assets/pk_profiles.json")
|
||||||
|
.takeIf { it.exists() }
|
||||||
|
?: java.io.File("app/src/main/assets/pk_profiles.json")
|
||||||
|
PKProfileStore.initWithJson(file.readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Traitement EEn (modèle TFS stocké), 5 mg à BASE. */
|
||||||
|
private fun treatment(): Treatment = Treatment(
|
||||||
|
id = 1L,
|
||||||
|
name = "EEn",
|
||||||
|
type = TreatmentType.ESTRADIOL,
|
||||||
|
route = AdministrationRoute.INJECTION_IM,
|
||||||
|
doseAmount = 5.0,
|
||||||
|
doseUnit = "mg",
|
||||||
|
isActive = true,
|
||||||
|
esterType = Esters.EEN,
|
||||||
|
pkModel = PKModels.TRANSFEM_SCIENCE
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun doses(): List<DoseLog> = listOf(
|
||||||
|
DoseLog(treatmentId = 1L, timestamp = BASE, doseAmount = 5.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Lab E2 de 250 pg/mL 7 jours après l'injection (au creux TFS). */
|
||||||
|
private fun labE2(): LabResult = LabResult(
|
||||||
|
marker = "E2", value = 250.0, unit = "pg/mL",
|
||||||
|
timestamp = BASE + 7 * DAY_MS
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all three models calibrated land on the lab (ESE included)`() {
|
||||||
|
val tr = treatment()
|
||||||
|
val labs = listOf(labE2())
|
||||||
|
for (model in listOf("ESE", "TFS", "WHS")) {
|
||||||
|
val scales = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = model
|
||||||
|
)
|
||||||
|
assertTrue("échelle $model absente", scales.containsKey("EEN"))
|
||||||
|
val calibrated = PharmacokineticEngine.e2At(
|
||||||
|
listOf(tr), doses(), labE2().timestamp,
|
||||||
|
modelOverride = model, scalePerEster = scales
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"$model : calibrée=$calibrated doit ≈ 250 (échelle ${scales["EEN"]})",
|
||||||
|
calibrated in 250.0 * 0.98..250.0 * 1.02
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `three models give three distinct calibration factors`() {
|
||||||
|
val tr = treatment()
|
||||||
|
val labs = listOf(labE2())
|
||||||
|
val ese = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = "ESE")["EEN"]!!
|
||||||
|
val tfs = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = "TFS")["EEN"]!!
|
||||||
|
val whs = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = "WHS")["EEN"]!!
|
||||||
|
// les prédictions des 3 modèles diffèrent au même instant (7 j) :
|
||||||
|
// ESE ≈ 156, TFS ≈ 159, WHS ≈ 178 → facteurs distincts. Nuances :
|
||||||
|
// ESE et TFS sont QUASI IDENTIQUES au creux 7 j (~2 % — la table ODS
|
||||||
|
// dérive du même article) ; WHSAH s'en écarte (~11 % — décroissance
|
||||||
|
// lente, t½ 7,3 j) mais sans excès
|
||||||
|
assertTrue("ESE=$ese vs TFS=$tfs (≥ 1 % : distincts)", kotlin.math.abs(ese - tfs) > 0.01 * ese)
|
||||||
|
assertTrue("TFS=$tfs vs WHS=$whs (≥ 5 %)", kotlin.math.abs(tfs - whs) > 0.05 * tfs)
|
||||||
|
assertTrue("WHS=$whs vs ESE=$ese (≥ 5 %)", kotlin.math.abs(whs - ese) > 0.05 * whs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `autoCalibrated delegates to computeEsterScaleFactors per model (single implementation)`() {
|
||||||
|
val tr = treatment()
|
||||||
|
val labs = listOf(labE2())
|
||||||
|
for (model in listOf(null, "ESE", "TFS", "WHS")) {
|
||||||
|
val viaAuto = PharmacokineticEngine.autoCalibrated(
|
||||||
|
listOf(tr), doses(), labs, TConfig(), modelOverride = model
|
||||||
|
).esterScales
|
||||||
|
val direct = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = model
|
||||||
|
)
|
||||||
|
assertEquals("délégation brisée pour modelOverride=$model", direct, viaAuto)
|
||||||
|
}
|
||||||
|
// null = modèle stocké du traitement (TFS) → cohérent avec override TFS
|
||||||
|
assertEquals(
|
||||||
|
PharmacokineticEngine.autoCalibrated(
|
||||||
|
listOf(tr), doses(), labs, TConfig(), modelOverride = "TFS"
|
||||||
|
).esterScales,
|
||||||
|
PharmacokineticEngine.autoCalibrated(
|
||||||
|
listOf(tr), doses(), labs, TConfig()
|
||||||
|
).esterScales
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `testosterone k is calibrated against the E2 of the requested model`() {
|
||||||
|
val tr = treatment()
|
||||||
|
// lab T de 0,35 ng/mL 7 jours après l'injection (même instant que le lab E2)
|
||||||
|
val labs = listOf(
|
||||||
|
labE2(),
|
||||||
|
LabResult(marker = "T", value = 0.35, unit = "ng/mL",
|
||||||
|
timestamp = BASE + 7 * DAY_MS)
|
||||||
|
)
|
||||||
|
val tConfig = TConfig() // base 6.0, floor 0.2, k 0.19
|
||||||
|
|
||||||
|
// Invariant mathématique : k ∝ 1/E2 calibrée — donner une échelle
|
||||||
|
// ×2 doit DIVISER le k par 2 exactement (le k est calculé contre
|
||||||
|
// l'E2 calibrée de la demande modelOverride, pas contre une E2 figée).
|
||||||
|
val tLabs = labs.filter { it.marker.equals("T", true) }
|
||||||
|
fun kFor(scale: Double): Double = PharmacokineticEngine.computeTKPerEster(
|
||||||
|
listOf(tr), doses(), tLabs, tConfig,
|
||||||
|
scalePerEster = mapOf("EEN" to scale),
|
||||||
|
modelOverride = "TFS"
|
||||||
|
)["EEN"]!!
|
||||||
|
val k1 = kFor(1.0)
|
||||||
|
val k2 = kFor(2.0)
|
||||||
|
assertTrue("k(échelle 1)=$k1 doit être dans la garde", k1 > 1e-4 && k1 < 10.0)
|
||||||
|
assertEquals("k ∝ 1/E2 calibrée : échelle ×2 → k ÷2", k1 / 2.0, k2, k1 * 0.01)
|
||||||
|
// et le k calibré ≠ k par défaut (0,19) : il a bien été recalculé
|
||||||
|
// contre l'E2 du modèle (fix #60)
|
||||||
|
assertTrue(
|
||||||
|
"k calibré=$k1 doit différer du k stocké (0,19)",
|
||||||
|
kotlin.math.abs(k1 - 0.19) > 0.01
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `labs before the first dose yield empty scales (per model too)`() {
|
||||||
|
val tr = treatment()
|
||||||
|
// lab ANTÉRIEUR à la 1ʳᵉ dose → pas de période d'attribution → vide
|
||||||
|
val labs = listOf(LabResult(
|
||||||
|
marker = "E2", value = 250.0, unit = "pg/mL", timestamp = BASE - DAY_MS
|
||||||
|
))
|
||||||
|
for (model in listOf(null, "ESE", "TFS", "WHS")) {
|
||||||
|
val scales = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses(), labs, modelOverride = model
|
||||||
|
)
|
||||||
|
assertTrue("échelles vides attendues pour modelOverride=$model", scales.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package com.hormonetrack.pk
|
|||||||
|
|
||||||
import com.hormonetrack.data.model.AdministrationRoute
|
import com.hormonetrack.data.model.AdministrationRoute
|
||||||
import com.hormonetrack.data.model.DoseLog
|
import com.hormonetrack.data.model.DoseLog
|
||||||
|
import com.hormonetrack.data.model.LabResult
|
||||||
import com.hormonetrack.data.model.Esters
|
import com.hormonetrack.data.model.Esters
|
||||||
import com.hormonetrack.data.model.PKModels
|
import com.hormonetrack.data.model.PKModels
|
||||||
import com.hormonetrack.data.model.Treatment
|
import com.hormonetrack.data.model.Treatment
|
||||||
@ -103,6 +104,53 @@ class WhsahEngineTest {
|
|||||||
assertTrue(curve.all { it.e2 == 0.0 })
|
assertTrue(curve.all { it.e2 == 0.0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calibration is per model - each calibrated curve lands on the lab (fix 60)`() {
|
||||||
|
// Remontée v1.4.7 : « la calibration auto sur WHSAH met ×2,21 » —
|
||||||
|
// AVANT : un seul jeu de facteurs calculé depuis le modèle STOCKÉ
|
||||||
|
// puis partagé par toutes les courbes. DÉSORMAIS (fix #60) : chaque
|
||||||
|
// modèle est calibré avec SA prédiction → chaque courbe calibrée
|
||||||
|
// passe PAR le lab, quel que soit le modèle affiché.
|
||||||
|
val tr = treatment(Esters.EEN, PKModels.TRANSFEM_SCIENCE)
|
||||||
|
val doses = listOf(
|
||||||
|
DoseLog(treatmentId = 1L, timestamp = BASE, doseAmount = 5.0)
|
||||||
|
)
|
||||||
|
// lab fictif de 250 pg/mL 7 jours après l'injection (au creux TFS)
|
||||||
|
val lab = LabResult(marker = "E2", value = 250.0, unit = "pg/mL",
|
||||||
|
timestamp = BASE + 7 * DAY_MS)
|
||||||
|
val tConfig = TConfig()
|
||||||
|
|
||||||
|
for ((model, expected) in listOf(
|
||||||
|
"TFS" to null, // attendu = calculé en dessous (auto-cohérence)
|
||||||
|
"WHS" to null
|
||||||
|
)) {
|
||||||
|
val scales = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses, listOf(lab), modelOverride = model
|
||||||
|
)
|
||||||
|
assertTrue("échelle $model doit être positive et plausible",
|
||||||
|
(scales["EEN"] ?: 0.0) > 0.0)
|
||||||
|
// la courbe calibrée avec SES échelles passe PAR le lab (±2 %)
|
||||||
|
val calibrated = PharmacokineticEngine.e2At(
|
||||||
|
listOf(tr), doses, lab.timestamp,
|
||||||
|
modelOverride = model, scalePerEster = scales
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"$model : calibrée=$calibrated doit ≈ lab=250 (échelle ${scales["EEN"]})",
|
||||||
|
calibrated in 250.0 * 0.98..250.0 * 1.02
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// et les échelles des DEUX modèles sont DISTINCTES (prédictions
|
||||||
|
// différentes au même instant → facteurs différents)
|
||||||
|
val scalesTfs = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses, listOf(lab), modelOverride = "TFS")
|
||||||
|
val scalesWhs = PharmacokineticEngine.computeEsterScaleFactors(
|
||||||
|
listOf(tr), doses, listOf(lab), modelOverride = "WHS")
|
||||||
|
assertTrue(
|
||||||
|
"TFS=${scalesTfs["EEN"]} vs WHS=${scalesWhs["EEN"]} doivent différer",
|
||||||
|
kotlin.math.abs(scalesTfs["EEN"]!! - scalesWhs["EEN"]!!) > 0.01
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calibration applies to ALL model curves equally (fix 58)`() {
|
fun `calibration applies to ALL model curves equally (fix 58)`() {
|
||||||
// Demande v1.4.7 : « TOUTES les courbes doivent s'adapter au calibrage »
|
// Demande v1.4.7 : « TOUTES les courbes doivent s'adapter au calibrage »
|
||||||
|
|||||||
@ -1,7 +1,36 @@
|
|||||||
# Changelog — HormoneTrack
|
# Changelog — HormoneTrack
|
||||||
|
|
||||||
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
|
||||||
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.7`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.8`, …).
|
||||||
|
|
||||||
|
## [1.4.8] — 2026-09-07 (versionCode 28)
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- **La calibration automatique était fausse pour les modèles autres que le
|
||||||
|
modèle stocké du traitement** (#60, remontée : « la calibration auto sur
|
||||||
|
WHSAH met ×2,21 alors que l'idéal serait < 1 ») : un SEUL jeu de facteurs
|
||||||
|
était calculé depuis la prédiction du modèle STOCKÉ puis partagé par
|
||||||
|
toutes les courbes — en basculant un traitement (ex. EEn TFS → WHSAH),
|
||||||
|
les facteurs devenaient absurdes (les prédictions WHSAH diffèrent de TFS :
|
||||||
|
creux plus profonds, montée plus rapide). FIX (demande : « la calibration
|
||||||
|
doit être active sur tous les modèles mais s'adapter à chaque profil ») :
|
||||||
|
**la calibration est désormais PAR MODÈLE** — le graphique calcule un jeu
|
||||||
|
de facteurs (et de k T) PAR modèle affiché (ESE/TFS/WHS), chacun calibré
|
||||||
|
avec la prédiction DE SON modèle → chaque courbe calibrée passe par tes
|
||||||
|
labs, quel que soit le modèle superposé. Home conserve la calibration du
|
||||||
|
modèle stocké (inchangé). Épinglé par test : chaque courbe calibrée passe
|
||||||
|
par le lab (±2 %) et les échelles TFS/WHS diffèrent.
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- **5 nouveaux tests** (164 au total / 142 sans les données locales) :
|
||||||
|
`CalibrationPerModelTest` — les 3 modèles calibrés atterrissent sur le
|
||||||
|
lab (ESE inclus), échelles distinctes (ESE↔TFS ~2 %, WHS ~11 %),
|
||||||
|
délégation `autoCalibrated(modelOverride)` → `computeEsterScaleFactors`
|
||||||
|
(implémentation unique), **k T par modèle** (k ∝ 1/E2 calibrée — et les
|
||||||
|
k post-calibration CONVERGENT entre modèles : même E2 cible, comportement
|
||||||
|
voulu), labs antérieurs à la 1ʳᵉ dose → échelles vides (par modèle).
|
||||||
|
|
||||||
|
## [1.4.7] — 2026-09-07 (versionCode 27)
|
||||||
|
|
||||||
## [1.4.7] — 2026-09-07 (versionCode 27)
|
## [1.4.7] — 2026-09-07 (versionCode 27)
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,7 @@ Fonctionnalités v1 :
|
|||||||
| 7 sept. 2026 (session v1.4.5) | **« La prévision simule le 13 au lieu du 12 »** (#55) : diagnostic sur données réelles — GÉNÉRATION EXACTE (créneau = dernière dose loguée + 6 j ; subtilité : la dose avait été loguée le LENDENDE de l'injection réelle → dérive d'un jour par cycle, l'app suit les logs) ; illusion causée par l'absence de marqueur + labels X à minuit UTC. FIX : marqueurs de doses (prévisionnels pointillés + réels discrets), labels à minuit LOCAL, fuseau de graphique configurable (Paramètres). **+ #56** : boucle de recomposition (`nowMs` relu à chaque frame → endMs dérive → saturation main thread → taps perdus) → mémoïsé sur le tick. 145 verts. Publication v1.4.5. |
|
| 7 sept. 2026 (session v1.4.5) | **« La prévision simule le 13 au lieu du 12 »** (#55) : diagnostic sur données réelles — GÉNÉRATION EXACTE (créneau = dernière dose loguée + 6 j ; subtilité : la dose avait été loguée le LENDENDE de l'injection réelle → dérive d'un jour par cycle, l'app suit les logs) ; illusion causée par l'absence de marqueur + labels X à minuit UTC. FIX : marqueurs de doses (prévisionnels pointillés + réels discrets), labels à minuit LOCAL, fuseau de graphique configurable (Paramètres). **+ #56** : boucle de recomposition (`nowMs` relu à chaque frame → endMs dérive → saturation main thread → taps perdus) → mémoïsé sur le tick. 145 verts. Publication v1.4.5. |
|
||||||
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAF épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
|
| 7 sept. 2026 (session v1.4.6) | **3ᵉ modèle PK « WHSAH »** (demande) : le fit « license-free » du WHSAH Collective publié dans **Mona** (l'app concurrente — son git montre qu'elle utilisait NOS paramètres TFS avant mars 2026) intégré au même niveau : dispatch parallèle (aucun remplacement), 6 presets, dropdown 3 choix, 3ᵉ toggle superposable (vert/violet), PEP non couvert. Comparaison chiffrée TFS vs WHSAF épinglée (EV pic 367 vs 295 ; EEn à J+1 ×3). **+ #57** : l'écran Graphiques défile désormais (rangée de toggles coupée sous le pli, trouvé en émulateur). 155 verts. Publication v1.4.6. |
|
||||||
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
|
| 7 sept. 2026 (session v1.4.7) | **Toggles alignés sur les traitements** (#58, demande) : au chargement, seuls les modèles utilisés par des traitements à PROFIL PK (`usesProfileModel`) sont ON (l'oral/AA polluait le set — vu en émulateur avec le CPA) ; garde-fou TFS si aucun profil ; une seule init. **Épinglé : la calibration s'applique à TOUTES les courbes** (par ESTER via doseEster, indépendant du modelOverride — test). 158 verts. Publication v1.4.7. | + **#59** : le chip de la carte de traitement affichait « Estrannaise » pour WHSAH (ternaire à 2 branches → `modelLabelRes` centralisé).
|
||||||
|
| 7 sept. 2026 (session v1.4.8) | **Calibration PAR MODÈLE PK** (#60, remontée : « ×2,21 sur WHSAH alors que l'idéal serait < 1 ») : un seul jeu de facteurs (calculé depuis le modèle stocké) était partagé par toutes les courbes → absurde dès qu'un traitement basculait de modèle. FIX : le graphique calcule une AutoCalibrated PAR modèle affiché (ESE/TFS/WHS — modelOverride propagé à computeEsterScaleFactors et computeTKPerEster) → chaque courbe calibrée passe par les labs. Home inchangé (modèle stocké). Test : chaque courbe calibrée ≈ lab (±2 %), échelles TFS ≠ WHS. 159 verts. Publication v1.4.8. |
|
||||||
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
| 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
| 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. |
|
||||||
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
| 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. |
|
||||||
@ -346,15 +347,15 @@ T est faux d'un facteur 100, bug réel remonté par l'utilisatrice : labs 33/44
|
|||||||
|
|
||||||
### 7.6 Calibration (v1.2.1 : PAR PÉRIODE D'ESTER pour l'auto)
|
### 7.6 Calibration (v1.2.1 : PAR PÉRIODE D'ESTER pour l'auto)
|
||||||
|
|
||||||
**PORTÉE (épinglé v1.4.7, test `calibration applies to ALL model curves
|
**PORTÉE (v1.4.8, fix #60) : la calibration est PAR MODÈLE PK.**
|
||||||
equally`)** : le facteur s'applique par ESTER de la DOSE (`doseEster`),
|
**PORTÉE (v1.4.8, fix #60)** : le facteur est calculé PAR MODÈLE PK — le
|
||||||
indépendamment du modelOverride → TOUTES les courbes affichées (ESE/TFS/
|
graphique calcule une `AutoCalibrated` par modèle affiché (ESE/TFS/WHS, via
|
||||||
WHS) d'un même traitement sont scalées identiquement. Subtilité : le
|
`autoCalibrated(modelOverride = …)`) : chaque courbe est calibrée avec la
|
||||||
facteur est calculé depuis la prédiction du modèle STOCKÉ du traitement —
|
prédiction DE SON modèle et passe par les labs quel que soit le modèle
|
||||||
les courbes des autres modèles partagent ce facteur (un facteur par
|
superposé (test `calibration is per model`). Home (sans courbes superposées)
|
||||||
ester/période, pas par modèle : les comparaisons inter-modèles reflètent
|
conserve la calibration du modèle STOCKÉ. HISTORIQUE : v1.4.7 partageait UN
|
||||||
la FORME, pas des calibrations distinctes).
|
seul facteur (calculé depuis le modèle stocké) entre toutes les courbes —
|
||||||
**Automatique** (`computeEsterScaleFactors` + `scalePerEster`, option « Auto-calibration ») :
|
absurde dès qu'un traitement changeait de modèle (×2,21 sur WHSAH).
|
||||||
- chaque lab est **attribué à la période d'injection dans laquelle il tombe** =
|
- chaque lab est **attribué à la période d'injection dans laquelle il tombe** =
|
||||||
dernière dose E2 ≤ lab (une prise de sang reflète d'abord l'injection qui précède) ;
|
dernière dose E2 ≤ lab (une prise de sang reflète d'abord l'injection qui précède) ;
|
||||||
- ratio = lab ÷ prédiction **non calibrée** (toutes doses superposées, scaleFactor forcé 1) ;
|
- ratio = lab ÷ prédiction **non calibrée** (toutes doses superposées, scaleFactor forcé 1) ;
|
||||||
@ -457,7 +458,7 @@ niveau que les deux autres, pas de remplacement »).
|
|||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**158 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **136 sans
|
**164 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **142 sans
|
||||||
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
les données de test locales** (cf §8.bis : les 3 classes de régression,
|
||||||
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
6 tests chacune, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
||||||
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'exécution =
|
||||||
@ -590,6 +591,18 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
traitement ESE, `pkModel = WHS`, coupure 10 × t½, PEP → 0) et **3 courbes
|
traitement ESE, `pkModel = WHS`, coupure 10 × t½, PEP → 0) et **3 courbes
|
||||||
EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont quasi
|
EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont quasi
|
||||||
identiques au pic par construction).
|
identiques au pic par construction).
|
||||||
|
- **`WhsahEngineTest`** (6) : + v1.4.8 — **calibration PAR MODÈLE** (fix
|
||||||
|
#60) : chaque courbe calibrée avec la prédiction de SON modèle passe par
|
||||||
|
le lab (±2 %) ; échelles TFS ≠ WHS au même lab.
|
||||||
|
- **`CalibrationPerModelTest`** (5) : v1.4.8 — **process calibration** :
|
||||||
|
les 3 modèles (ESE inclus) calibrés atterrissent sur le lab ; échelles
|
||||||
|
distinctes (ESE↔TFS ~2 %, WHS ~11 % — nuance documentée : la table ODS
|
||||||
|
dérive du même article) ; **délégation** `autoCalibrated(modelOverride)`
|
||||||
|
→ `computeEsterScaleFactors` (implémentation unique — Home et graphique
|
||||||
|
ne peuvent pas diverger) ; **k T par modèle** (k ∝ 1/E2 calibrée :
|
||||||
|
échelle ×2 → k ÷2 ; les k post-calibration convergent entre modèles —
|
||||||
|
même E2 cible) ; labs antérieurs à la 1ʳᵉ dose → échelles vides (par
|
||||||
|
modèle). Toute retouche du pipeline de calibration passe par CE test.
|
||||||
- **`ChartZoomTest`** (15) : v1.2.9 (pas d'échantillonnage adaptatif) /
|
- **`ChartZoomTest`** (15) : v1.2.9 (pas d'échantillonnage adaptatif) /
|
||||||
v1.4.1 (horizon + clamp panoramique) / v1.4.5 (`pointHoursBefore`,
|
v1.4.1 (horizon + clamp panoramique) / v1.4.5 (`pointHoursBefore`,
|
||||||
`xLabelTicks` minuit local) / v1.4.7 — **`defaultModelToggles`** (toggles
|
`xLabelTicks` minuit local) / v1.4.7 — **`defaultModelToggles`** (toggles
|
||||||
@ -727,7 +740,9 @@ Sur la montre : remontée par Gadgetbridge **ou** Huawei Health (cf §17).
|
|||||||
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
(+ FAB), mini-chart 24 h (multi-séries via `ChartSeries`) **cliquable → écran
|
||||||
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
Graphiques** (v1.2.1) avec mini-légende E2/T ; données auto-calibrées si l'option est
|
||||||
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
active (`scalePerEster = effectiveAuto?.esterScales`) ; rafraîchissement `tick` 60 s
|
||||||
- `ChartScreen` (v1.2, le plus riche) : **toggles de modèles initialement
|
- `ChartScreen` (v1.2, le plus riche) : **calibration PAR MODÈLE** (v1.4.8,
|
||||||
|
fix #60 : `autoByModel` — une AutoCalibrated par modèle affiché, chaque
|
||||||
|
courbe reçoit SES échelles/k) ; **toggles de modèles initialement
|
||||||
alignés sur les traitements** (v1.4.7, fix #58 : `defaultModelToggles` sur
|
alignés sur les traitements** (v1.4.7, fix #58 : `defaultModelToggles` sur
|
||||||
les pkModel des traitements à PROFIL PK — init unique au chargement ;
|
les pkModel des traitements à PROFIL PK — init unique au chargement ;
|
||||||
garde-fou TFS si aucun profil — les traitements Bateman sont tracés dans
|
garde-fou TFS si aucun profil — les traitements Bateman sont tracés dans
|
||||||
@ -1372,8 +1387,8 @@ vérifie pas en tests JVM).
|
|||||||
|
|
||||||
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
1. **Bumper la version** dans `app/build.gradle.kts` : `versionCode = N+1`,
|
||||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 158 au
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 164 au
|
||||||
total, 136 si `local-test-data/` est absent (les 3 classes de régression
|
total, 142 si `local-test-data/` est absent (les 3 classes de régression
|
||||||
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
réelles sont skippées via `Assume`, 6 tests chacune) ; **lint vert
|
||||||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||||
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
attrapé les crashs #47/#48 dès v1.3.1). Paliers de test du projet :
|
||||||
@ -1676,13 +1691,17 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
|
- [ ] **v1.4.6** : **3ᵉ modèle WHSAH** : preset WHSAH → courbe verte
|
||||||
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
|
superposable aux 2 autres (montée plus rapide à J+1, t½ plus longues
|
||||||
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
|
que TFS) ; PEP non couvert ; 6 presets ; dropdown modèle à 3 choix
|
||||||
|
- [ ] **v1.4.8** : **calibration par modèle** : superposer TFS + WHSAH avec
|
||||||
|
l'auto-calibration ON → CHAQUE courbe colle aux labs (plus de facteur
|
||||||
|
partagé absurde type ×2,21 — fix #60)
|
||||||
|
|
||||||
- [ ] **v1.4.7** : **toggles alignés sur les traitements** : au 1ᵉʳ chargement
|
- [ ] **v1.4.7** : **toggles alignés sur les traitements** : au 1ᵉʳ chargement
|
||||||
du graphique, seuls les modèles des traitements à profil sont ON
|
du graphique, seuls les modèles des traitements à profil sont ON
|
||||||
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
||||||
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
tap ; et TOUTES les courbes affichées reflètent la calibration (test)
|
||||||
|
|
||||||
---
|
---
|
||||||
*Doc mise à jour le 7 sept. 2026 (v1.4.7) — build OK, lint vert, 158/158 tests verts (136 sans
|
*Doc mise à jour le 7 sept. 2026 (v1.4.8) — build OK, lint vert, 164/164 tests verts (142 sans
|
||||||
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée
|
||||||
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de
|
||||||
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs
|
||||||
|
|||||||
@ -206,7 +206,10 @@ Pics de référence (dose unique de 5 mg, IM) :
|
|||||||
l'affichage seulement, tes réglages stockés ne changent pas. **Chaque ester est
|
l'affichage seulement, tes réglages stockés ne changent pas. **Chaque ester est
|
||||||
calibré avec les labs faits pendant sa période** : si tu étais sous valerate avant
|
calibré avec les labs faits pendant sa période** : si tu étais sous valerate avant
|
||||||
d'être sous enanthate, tes labs valerate calibrent les doses valerate (E2 **et**
|
d'être sous enanthate, tes labs valerate calibrent les doses valerate (E2 **et**
|
||||||
la suppression T), et inversement pour l'enanthate
|
la suppression T), et inversement pour l'enanthate. **Calibration par modèle**
|
||||||
|
(v1.4.8) : chaque modèle affiché dans le graphique (Estrannaise / Transfem
|
||||||
|
Science / WHSAH) reçoit SES propres facteurs calculés sur tes labs — les
|
||||||
|
courbes superposées collent toutes à tes résultats, quelle que soit leur forme
|
||||||
|
|
||||||
## 9. Dépannage
|
## 9. Dépannage
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user