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
22d4c66d81
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
|
||||
> 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)
|
||||
- **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)
|
||||
@ -37,7 +37,8 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
||||
[article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)),
|
||||
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
|
||||
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,
|
||||
patch et voie orale
|
||||
- **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
|
||||
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 # 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
|
||||
```
|
||||
|
||||
@ -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) —
|
||||
`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)) :
|
||||
`-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
|
||||
|
||||
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
|
||||
du `.ods` (8001 h) ; **Transfem Science** = méta-analyse à 3 compartiments (V3C,
|
||||
forme close, [article](https://transfemscience.org/articles/injectable-e2-meta-analysis/)).
|
||||
(pg/mL par mg) ; les contributions se superposent. **Trois modèles superposables** :
|
||||
|
||||
- **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) :
|
||||
|
||||
| Profil | Modèle | Pic (pg/mL/mg) | Tmax |
|
||||
|----------|------------------|----------------|--------|
|
||||
| EV | Estrannaise | 61,1 | ~45 h |
|
||||
| EU | Estrannaise | 3,4 | ~55 h (plateau long) |
|
||||
| EEn | Estrannaise | 31,4 | ~152 h |
|
||||
| EV | Transfem Science | 59,0 | ~51 h |
|
||||
| EU | Transfem Science | 10,1 | ~198 h |
|
||||
| EEn | Transfem Science | 32,0 | ~156 h |
|
||||
| EB | Transfem Science | 194,2 | ~16 h |
|
||||
| EC (huile) | Transfem Science | 31,1 | ~103 h |
|
||||
| PEP | Transfem Science | 1,03 (dose ~6,5×) | ~18 j |
|
||||
| Profil | Modèle | Pic (pg/mL/mg) | Tmax | t½ term. |
|
||||
|----------|------------------|----------------|--------|----------|
|
||||
| EV | Estrannaise | 61,1 | ~45 h | — |
|
||||
| EU | Estrannaise | 3,4 | ~55 h (plateau long) | — |
|
||||
| EEn | Estrannaise | 31,4 | ~152 h | — |
|
||||
| EV | Transfem Science | 59,0 | ~51 h | 3,0 j |
|
||||
| EU | Transfem Science | 10,1 | ~198 h | — |
|
||||
| EEn | Transfem Science | 32,0 | ~156 h | 4,5 j |
|
||||
| EB | Transfem Science | 194,2 | ~16 h | 1,2 j |
|
||||
| EC (huile) | Transfem Science | 31,1 | ~103 h | 6,7 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
|
||||
à ton corps, exactement comme la colonne « Scale factor » de la feuille d'origine.
|
||||
**Calibration (v1.4.8)** : le facteur d'échelle s'applique par ESTER et par
|
||||
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
|
||||
|
||||
|
||||
@ -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 = 27
|
||||
versionName = "1.4.7"
|
||||
versionCode = 28
|
||||
versionName = "1.4.8"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -397,7 +397,8 @@ object PharmacokineticEngine {
|
||||
fun computeEsterScaleFactors(
|
||||
treatments: List<Treatment>,
|
||||
doseLogs: List<DoseLog>,
|
||||
e2Labs: List<LabResult>
|
||||
e2Labs: List<LabResult>,
|
||||
modelOverride: String? = null
|
||||
): Map<String, Double> {
|
||||
val estrogenTreatments = treatments.filter { it.type == TreatmentType.ESTRADIOL }
|
||||
val estrogenDoses = doseLogs.filter { d ->
|
||||
@ -405,11 +406,15 @@ object PharmacokineticEngine {
|
||||
}
|
||||
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 ratiosByEster = HashMap<String, MutableList<Double>>()
|
||||
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
|
||||
|
||||
// Attribution : dernière dose E2 ≤ lab → son ester (override compris)
|
||||
@ -451,7 +456,8 @@ object PharmacokineticEngine {
|
||||
doseLogs: List<DoseLog>,
|
||||
tLabs: List<LabResult>,
|
||||
current: TConfig,
|
||||
scalePerEster: Map<String, Double>? = null
|
||||
scalePerEster: Map<String, Double>? = null,
|
||||
modelOverride: String? = null
|
||||
): Map<String, Double> {
|
||||
val estrogenDoses = doseLogs.filter { d ->
|
||||
treatments.any { it.id == d.treatmentId && it.type == TreatmentType.ESTRADIOL }
|
||||
@ -469,7 +475,9 @@ object PharmacokineticEngine {
|
||||
val ester = doseEster(tr, attributed)
|
||||
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
|
||||
val k = ((current.base - current.floor) / (tNgMl - current.floor) - 1.0) / e2
|
||||
if (k > 1e-4 && k < 10.0) {
|
||||
@ -511,14 +519,23 @@ object PharmacokineticEngine {
|
||||
treatments: List<Treatment>,
|
||||
doseLogs: List<DoseLog>,
|
||||
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 {
|
||||
val e2Labs = labs.filter { it.marker.equals("E2", 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(
|
||||
treatments, doseLogs, tLabs, tConfig, scalePerEster = esterScales
|
||||
treatments, doseLogs, tLabs, tConfig, scalePerEster = esterScales,
|
||||
modelOverride = modelOverride
|
||||
)
|
||||
return AutoCalibrated(
|
||||
treatments = treatments,
|
||||
|
||||
@ -146,14 +146,34 @@ fun ChartScreen() {
|
||||
|
||||
// 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.
|
||||
val effectiveAuto by produceState<PharmacokineticEngine.AutoCalibrated?>(
|
||||
null, treatments, doseLogs, labResults, tConfig, autoCalibrate
|
||||
// ── Calibration PAR MODÈLE (v1.4.8, fix #60) ──────────────────────────────
|
||||
// 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) {
|
||||
null
|
||||
emptyMap()
|
||||
} else {
|
||||
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")
|
||||
val curves by produceState(
|
||||
emptyList<Pair<String, List<LevelPoint>>>(),
|
||||
effectiveAuto, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast
|
||||
autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast
|
||||
) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
||||
if (showEse || showTfs || showWhs) {
|
||||
// Valeurs actuelles : échelles par ester si auto-calibration active,
|
||||
// sinon le scaleFactor stocké de chaque traitement
|
||||
val effTreatments = effectiveAuto?.treatments ?: treatments
|
||||
val effTConfig = effectiveAuto?.tConfig ?: tConfig
|
||||
val effScales = effectiveAuto?.esterScales
|
||||
// Calibration PAR MODÈLE (fix #60) : chaque courbe reçoit les
|
||||
// échelles/k calculés avec SA prédiction. Hors auto-calibration
|
||||
// → scaleFactor stocké des traitements (comportement historique).
|
||||
val effTreatments = autoByModel["TFS"]?.treatments ?: treatments
|
||||
val effTConfig = autoByModel["TFS"]?.tConfig ?: tConfig
|
||||
// Doses réelles + doses prévisionnelles éventuelles
|
||||
val forecastDoses = if (showForecast) {
|
||||
effTreatments.flatMap {
|
||||
@ -249,36 +269,39 @@ fun ChartScreen() {
|
||||
// Pas d'échantillonnage adaptatif au zoom (v1.2.9)
|
||||
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
||||
if (showEse) {
|
||||
val autoM = autoByModel["ESE"]
|
||||
list.add(
|
||||
"ESE" to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = effTConfig, modelOverride = "ESE",
|
||||
scalePerEster = effScales,
|
||||
tKPerEster = effectiveAuto?.tKPerEster
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "ESE",
|
||||
scalePerEster = autoM?.esterScales,
|
||||
tKPerEster = autoM?.tKPerEster
|
||||
)
|
||||
)
|
||||
}
|
||||
if (showTfs) {
|
||||
val autoM = autoByModel["TFS"]
|
||||
list.add(
|
||||
"TFS" to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = effTConfig, modelOverride = "TFS",
|
||||
scalePerEster = effScales,
|
||||
tKPerEster = effectiveAuto?.tKPerEster
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "TFS",
|
||||
scalePerEster = autoM?.esterScales,
|
||||
tKPerEster = autoM?.tKPerEster
|
||||
)
|
||||
)
|
||||
}
|
||||
// v1.4.6 : 3ᵉ modèle WHSAH (même mécanique d'override)
|
||||
if (showWhs) {
|
||||
val autoM = autoByModel["WHS"]
|
||||
list.add(
|
||||
"WHS" to PharmacokineticEngine.computeCurve(
|
||||
effTreatments, allDoses, startMs, endMs,
|
||||
stepMs = step,
|
||||
tConfig = effTConfig, modelOverride = "WHS",
|
||||
scalePerEster = effScales,
|
||||
tKPerEster = effectiveAuto?.tKPerEster
|
||||
tConfig = autoM?.tConfig ?: effTConfig, modelOverride = "WHS",
|
||||
scalePerEster = autoM?.esterScales,
|
||||
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.DoseLog
|
||||
import com.hormonetrack.data.model.LabResult
|
||||
import com.hormonetrack.data.model.Esters
|
||||
import com.hormonetrack.data.model.PKModels
|
||||
import com.hormonetrack.data.model.Treatment
|
||||
@ -103,6 +104,53 @@ class WhsahEngineTest {
|
||||
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
|
||||
fun `calibration applies to ALL model curves equally (fix 58)`() {
|
||||
// Demande v1.4.7 : « TOUTES les courbes doivent s'adapter au calibrage »
|
||||
|
||||
@ -1,7 +1,36 @@
|
||||
# Changelog — HormoneTrack
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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.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.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. |
|
||||
| 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. |
|
||||
@ -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)
|
||||
|
||||
**PORTÉE (épinglé v1.4.7, test `calibration applies to ALL model curves
|
||||
equally`)** : le facteur s'applique par ESTER de la DOSE (`doseEster`),
|
||||
indépendamment du modelOverride → TOUTES les courbes affichées (ESE/TFS/
|
||||
WHS) d'un même traitement sont scalées identiquement. Subtilité : le
|
||||
facteur est calculé depuis la prédiction du modèle STOCKÉ du traitement —
|
||||
les courbes des autres modèles partagent ce facteur (un facteur par
|
||||
ester/période, pas par modèle : les comparaisons inter-modèles reflètent
|
||||
la FORME, pas des calibrations distinctes).
|
||||
**Automatique** (`computeEsterScaleFactors` + `scalePerEster`, option « Auto-calibration ») :
|
||||
**PORTÉE (v1.4.8, fix #60) : la calibration est PAR MODÈLE PK.**
|
||||
**PORTÉE (v1.4.8, fix #60)** : le facteur est calculé PAR MODÈLE PK — le
|
||||
graphique calcule une `AutoCalibrated` par modèle affiché (ESE/TFS/WHS, via
|
||||
`autoCalibrated(modelOverride = …)`) : chaque courbe est calibrée avec la
|
||||
prédiction DE SON modèle et passe par les labs quel que soit le modèle
|
||||
superposé (test `calibration is per model`). Home (sans courbes superposées)
|
||||
conserve la calibration du modèle STOCKÉ. HISTORIQUE : v1.4.7 partageait UN
|
||||
seul facteur (calculé depuis le modèle stocké) entre toutes les courbes —
|
||||
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** =
|
||||
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) ;
|
||||
@ -457,7 +458,7 @@ niveau que les deux autres, pas de remplacement »).
|
||||
|
||||
## 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,
|
||||
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 =
|
||||
@ -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
|
||||
EV distinctes** (ESE/TFS/WHS — seuil 2 % : ESE et TFS sont quasi
|
||||
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) /
|
||||
v1.4.1 (horizon + clamp panoramique) / v1.4.5 (`pointHoursBefore`,
|
||||
`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
|
||||
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
|
||||
- `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
|
||||
les pkModel des traitements à PROFIL PK — init unique au chargement ;
|
||||
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`,
|
||||
`versionName = "X.Y.Z+1"` (SemVer : fix = Z, feature = Y).
|
||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 158 au
|
||||
total, 136 si `local-test-data/` est absent (les 3 classes de régression
|
||||
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 164 au
|
||||
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
|
||||
obligatoire** : `./gradlew lint` (v1.3.4 — `StringFormatMatches` aurait
|
||||
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
|
||||
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
|
||||
- [ ] **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
|
||||
du graphique, seuls les modèles des traitements à profil sont ON
|
||||
(ex. EV TFS + EEn WHSAH → Estrannaise OFF) ; les autres activables au
|
||||
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
|
||||
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
|
||||
|
||||
@ -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
|
||||
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**
|
||||
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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user