Compare commits
No commits in common. "0768b83a88c2d3049fe411f6b984df1b2dcc3ceb" and "dd5a9e3013ae0266cfc297bbf739abf8c02c3304" have entirely different histories.
0768b83a88
...
dd5a9e3013
20
README.md
20
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.6.0 — build Android ✅, **lint vert** ✅, **191 tests unitaires** ✅ (168 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
- **Statut** : v1.5.0 — build Android ✅, **lint vert** ✅, **183 tests unitaires** ✅ (160 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
|
||||||
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **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)
|
||||||
@ -39,15 +39,13 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
|
|||||||
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 **chaque modèle calibré
|
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)
|
séparément** par tes labs (v1.4.8 — les facteurs s'adaptent à chaque profil)
|
||||||
- **« Tracé labs » (v1.5.0, prolongé en v1.6.0)** : la courbe ancrée sur tes
|
- **« Tracé labs » (v1.5.0)** : la courbe ancrée sur tes prises de sang
|
||||||
prises de sang (chip `Tracé labs`, off par défaut) — la forme du modèle PK
|
(chip `Tracé labs`, off par défaut) — la forme du modèle PK entre les
|
||||||
entre les labs, mais l'amplitude recalée pour passer EXACTEMENT sur chaque
|
labs, mais l'amplitude recalée pour passer EXACTEMENT sur chaque lab E2
|
||||||
lab E2 ; transition log-linéaire d'un lab au suivant, garde des labs hors
|
; transition log-linéaire d'un lab au suivant, garde des labs hors
|
||||||
fenêtre d'action (#61) ; **chip `Prolonger` (v1.6.0)** : la courbe se
|
fenêtre d'action (#61), fenêtre bornée [1er ; dernier lab] — la courbe
|
||||||
prolonge au-delà du dernier lab (modèle × ratio du dernier lab, horizon =
|
est une référence de COMPARAISON, jamais une estimation d'action (elle
|
||||||
extinction du modèle, partie estimée dessinée atténuée + légende dédiée) —
|
n'entre jamais dans l'accueil/les alertes)
|
||||||
la courbe est une référence de COMPARAISON, jamais une estimation d'action
|
|
||||||
(elle n'entre jamais dans l'accueil/les alertes)
|
|
||||||
- **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
|
||||||
@ -102,7 +100,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 # 191 tests (168 sans les données locales)
|
./gradlew testDebugUnitTest # 183 tests (160 sans les données locales)
|
||||||
./gradlew lint # lint vert obligatoire avant release
|
./gradlew lint # lint vert obligatoire avant release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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 = 32
|
versionCode = 31
|
||||||
versionName = "1.6.0"
|
versionName = "1.5.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@ -38,12 +38,11 @@ import kotlin.math.ln
|
|||||||
* PORTÉE ET LIMITES (essentielles — cf doc §7.10 / décision plan v1.5.0) :
|
* PORTÉE ET LIMITES (essentielles — cf doc §7.10 / décision plan v1.5.0) :
|
||||||
* - **E2 uniquement** (les labs T : 1) sont trop rares, 2) leurs unités
|
* - **E2 uniquement** (les labs T : 1) sont trop rares, 2) leurs unités
|
||||||
* variées ont causé le bug #26 — hors scope) ;
|
* variées ont causé le bug #26 — hors scope) ;
|
||||||
* - **fenêtre = [1er lab significatif ; dernier lab]** par défaut — rien
|
* - **fenêtre = [1er lab significatif ; dernier lab]** — rien AVANT (M=0
|
||||||
* AVANT (M=0 sans dose) ni APRÈS (aucune extrapolation) ;
|
* sans dose) ni APRÈS (aucune extrapolation : le vieillissement après le
|
||||||
* - **PROLONGATION (v1.6.0, opt-in `extendBeyondLastLab = true`)** : la
|
* dernier lab est visible = honnête, l'app n'invente pas de niveau) ;
|
||||||
* courbe peut se PROLONGER au-delà du dernier lab — cf détail ci-dessous ;
|
|
||||||
* - < 2 labs significatifs → courbe vide (un lab seul ne définit pas
|
* - < 2 labs significatifs → courbe vide (un lab seul ne définit pas
|
||||||
* d'intervalle) — et donc jamais de prolongation non plus ;
|
* d'intervalle) ;
|
||||||
* - indépendante de la calibration : cette courbe EST SA PROPRE
|
* - indépendante de la calibration : cette courbe EST SA PROPRE
|
||||||
* calibration continue (ratio brut, scaleFactor des traitements ignoré
|
* calibration continue (ratio brut, scaleFactor des traitements ignoré
|
||||||
* ET scalePerEster ignoré — l'inverse serait une double correction,
|
* ET scalePerEster ignoré — l'inverse serait une double correction,
|
||||||
@ -51,63 +50,13 @@ import kotlin.math.ln
|
|||||||
* - **display-end only** : utilisée UNIQUEMENT par le graphique (chip,
|
* - **display-end only** : utilisée UNIQUEMENT par le graphique (chip,
|
||||||
* off par défaut). Jamais branchée sur `levelAt` — l'accueil, les
|
* off par défaut). Jamais branchée sur `levelAt` — l'accueil, les
|
||||||
* seuils d'alerte et les rappels continuent d'utiliser le modèle
|
* seuils d'alerte et les rappels continuent d'utiliser le modèle
|
||||||
* calibré (invariant v1.4.2 §9.bis). La partie PROLONGÉE ne fait pas
|
* calibré (invariant v1.4.2 §9.bis).
|
||||||
* exception : elle est purement visuelle (série "LABX" du chart).
|
|
||||||
* - courbes T non modélisées ici : la série n'a pas de tStyle (le chart
|
* - courbes T non modélisées ici : la série n'a pas de tStyle (le chart
|
||||||
* ne dessine pas de T pour la série — cf CurveChart).
|
* ne dessine pas de T pour la série — cf CurveChart).
|
||||||
*
|
*
|
||||||
* PUR (aucune dépendance Android) → testable en JVM
|
* PUR (aucune dépendance Android) → testable en JVM
|
||||||
* (LabTrajectoryModelTest — l'ancrage exact, la garde #61 exactement,
|
* (LabTrajectoryModelTest — l'ancrage exact, la garde #61 exactement,
|
||||||
* les bornes de fenêtre, la prolongation).
|
* les bornes de fenêtre).
|
||||||
*
|
|
||||||
* ── PROLONGATION AU-DELÀ DU DERNIER LAB (v1.6.0) ──────────────────────────
|
|
||||||
*
|
|
||||||
* DEMANDE (v1.6.0) : « permettre au tracé labs de s'étendre au-delà du
|
|
||||||
* dernier lab, en simulant à partir des labs précédents, du dosage et du
|
|
||||||
* type d'ester injecté, et de l'évolution classique de l'ester ».
|
|
||||||
*
|
|
||||||
* Le constat v1.5.0 : la fenêtre s'arrêtait AU dernier lab — entre ce lab
|
|
||||||
* et « maintenant » (voire au-delà, avec la prévision), la courbe n'existait
|
|
||||||
* pas, alors que c'est précisément la période la plus récente qu'on veut
|
|
||||||
* comparer aux courbes des modèles.
|
|
||||||
*
|
|
||||||
* PRINCIPE : après le dernier ancrage, ρ cesse d'interpoler et reste CONSTANT :
|
|
||||||
*
|
|
||||||
* pour t > t_last_lab : courbe(t) = M(t) × ρ_last
|
|
||||||
*
|
|
||||||
* Justification, composant par composant (la demande, point par point) :
|
|
||||||
* - **« l'évolution classique de l'ester »** = M(t) : la forme du modèle PK
|
|
||||||
* (montée, Tmax, queues, accumulation) porte toute la dynamique ;
|
|
||||||
* - **« le dosage et le type d'ester injecté »** = M(t) inclut AUTOMATIQUEMENT
|
|
||||||
* les doses loguées après le dernier lab : une nouvelle injection (même à
|
|
||||||
* un autre ester) fait repartir la courbe en pic, à l'amplitude calibrée ;
|
|
||||||
* - **« les données de labs précédentes »** = ρ_last : le rapport
|
|
||||||
* corps-réel ÷ modèle observé au dernier lab est supposé rester valable.
|
|
||||||
*
|
|
||||||
* POURQUOI ρ CONSTANT et pas une extrapolation de la PENTE de ρ : la pente
|
|
||||||
* entre deux labs ne mesure pas une tendance du corps, elle morphing deux
|
|
||||||
* corrections d'amplitude — l'extrapoler divergerait sans aucune base
|
|
||||||
* physiologique (un ratio de calibration qui monte de 5 %/jour finirait par
|
|
||||||
* des facteurs ×10 inventés). La calibration observée au dernier lab est le
|
|
||||||
* meilleur estimateur disponible pour la suite ; c'est exactement ce que
|
|
||||||
* fait la calibration classique (le facteur stocké s'applique aux doses
|
|
||||||
* futures — §7.6).
|
|
||||||
*
|
|
||||||
* BORNE DE PROLONGATION (horizon) : la courbe prolongée s'arrête quand le
|
|
||||||
* MODÈLE lui-même s'éteint — `extensionHorizonEndMs` = dernière dose E2 +
|
|
||||||
* [PharmacokineticEngine.cutoffHours] de son traitement (10 t½ terminales pour V3C/WHS, fin de
|
|
||||||
* table pour ODS, 30 t½ Bateman). Au-delà, M(t) = 0 : dessiner plus serait
|
|
||||||
* une ligne à zéro purement inventée. La prolongation est en plus clippée
|
|
||||||
* par la fenêtre DEMANDÉE (celle du graphique).
|
|
||||||
*
|
|
||||||
* LIMITES ASSUMÉES (documentées, pas de bug — cf §7.10.bis) :
|
|
||||||
* - changement d'ESTER après le dernier lab : ρ_last a été mesuré sous
|
|
||||||
* l'ancien ester ; l'amplitude du nouveau reste calibrée par ce ρ —
|
|
||||||
* même limite que la calibration classique (fallback du facteur stocké,
|
|
||||||
* pas de lab de la nouvelle période) ;
|
|
||||||
* - les doses PRÉVISIONNELLES ne participent PAS : la prolongation ne
|
|
||||||
* consomme que les doses RÉELLES (le futur projeté reste la série
|
|
||||||
* « Prévision », qui superpose déjà les modèles calibrés).
|
|
||||||
*/
|
*/
|
||||||
object LabTrajectoryModel {
|
object LabTrajectoryModel {
|
||||||
|
|
||||||
@ -115,20 +64,10 @@ object LabTrajectoryModel {
|
|||||||
* Résultat : les points (E2 ancrée, t = 0 — pas de courbe T pour cette
|
* Résultat : les points (E2 ancrée, t = 0 — pas de courbe T pour cette
|
||||||
* série) + le nombre de labs réellement utilisés comme ancrages
|
* série) + le nombre de labs réellement utilisés comme ancrages
|
||||||
* (diagnostic dans la doc ; 0 → courbe vide).
|
* (diagnostic dans la doc ; 0 → courbe vide).
|
||||||
*
|
|
||||||
* [lastAnchorMs] (v1.6.0) : timestamp du DERNIER lab significatif (le
|
|
||||||
* dernier point « réel » de la courbe). `null` si < 2 ancres (courbe
|
|
||||||
* vide). L'écran Graphiques l'utilise pour SPLITTER la série en deux
|
|
||||||
* traits distincts :
|
|
||||||
* - "LAB" = points ≤ lastAnchorMs (ancré — passe par les labs) ;
|
|
||||||
* - "LABX" = points > lastAnchorMs (prolongé — ρ constant, estimation).
|
|
||||||
* La distinction visuelle préserve l'honnêteté de l'affichage : la partie
|
|
||||||
* inventée ne peut pas se confondre avec la partie mesurée.
|
|
||||||
*/
|
*/
|
||||||
data class AnchoredCurve(
|
data class AnchoredCurve(
|
||||||
val points: List<LevelPoint>,
|
val points: List<LevelPoint>,
|
||||||
val anchoredLabs: Int,
|
val anchoredLabs: Int
|
||||||
val lastAnchorMs: Long? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Les ancrages sont des couples (timestamp du lab, ratio lab ÷ prédiction)
|
// Les ancrages sont des couples (timestamp du lab, ratio lab ÷ prédiction)
|
||||||
@ -140,28 +79,15 @@ object LabTrajectoryModel {
|
|||||||
* @param treatments TOUS les traitements (actifs ET inactifs — §6.bis :
|
* @param treatments TOUS les traitements (actifs ET inactifs — §6.bis :
|
||||||
* l'inactivation ne retire pas l'historique de la
|
* l'inactivation ne retire pas l'historique de la
|
||||||
* prédiction M(t))
|
* prédiction M(t))
|
||||||
* @param doseLogs toutes les doses (RÉELLES uniquement — les doses
|
* @param doseLogs toutes les doses
|
||||||
* prévisionnelles ne participent jamais au tracé labs ;
|
|
||||||
* les doses loguées APRÈS le dernier lab sont au
|
|
||||||
* contraire INCLUSES : elles portent la forme de la
|
|
||||||
* prolongation, cf KDoc de l'objet)
|
|
||||||
* @param e2Labs labs E2 uniquement (les marqueurs autres sont
|
* @param e2Labs labs E2 uniquement (les marqueurs autres sont
|
||||||
* ignorés par `marker == "E2"`) — unité pg/mL
|
* ignorés par `marker == "E2"`) — unité pg/mL
|
||||||
* @param startMs/endMs fenêtre demandée (celle du graphique) — la courbe
|
* @param startMs/endMs fenêtre demandée (celle du graphique) — la courbe
|
||||||
* est CLIPPÉE à l'intersection avec [lab 1er ; lab
|
* est CLIPPÉE à l'intersection avec [lab 1er ; lab dernier]
|
||||||
* dernier], ou avec [lab 1er ; horizon de prolongation]
|
|
||||||
* quand [extendBeyondLastLab] est actif
|
|
||||||
* @param stepMs pas d'échantillonnage (stepForRange, v1.2.9)
|
* @param stepMs pas d'échantillonnage (stepForRange, v1.2.9)
|
||||||
* @param modelOverride modèle pour la prédiction M(t) (défaut = modèle
|
* @param modelOverride modèle pour la prédiction M(t) (défaut = modèle
|
||||||
* stocké, même sémantique que l'auto-calibration
|
* stocké, même sémantique que l'auto-calibration
|
||||||
* Home v1.4.8)
|
* Home v1.4.8)
|
||||||
* @param extendBeyondLastLab PROLONGATION (v1.6.0, opt-in) : au-delà du
|
|
||||||
* dernier lab significatif, ρ reste CONSTANT
|
|
||||||
* (= ρ du dernier lab) et la courbe continue
|
|
||||||
* `M(t) × ρ_last` jusqu'à
|
|
||||||
* [extensionHorizonEndMs] ∩ fenêtre demandée.
|
|
||||||
* false (défaut) = comportement v1.5.0 exact :
|
|
||||||
* la fenêtre s'arrête au dernier lab.
|
|
||||||
*/
|
*/
|
||||||
fun computeLabAnchoredCurve(
|
fun computeLabAnchoredCurve(
|
||||||
treatments: List<Treatment>,
|
treatments: List<Treatment>,
|
||||||
@ -170,8 +96,7 @@ object LabTrajectoryModel {
|
|||||||
startMs: Long,
|
startMs: Long,
|
||||||
endMs: Long,
|
endMs: Long,
|
||||||
stepMs: Long = 3_600_000L,
|
stepMs: Long = 3_600_000L,
|
||||||
modelOverride: String? = null,
|
modelOverride: String? = null
|
||||||
extendBeyondLastLab: Boolean = false
|
|
||||||
): AnchoredCurve {
|
): AnchoredCurve {
|
||||||
if (startMs >= endMs) return AnchoredCurve(emptyList(), 0)
|
if (startMs >= endMs) return AnchoredCurve(emptyList(), 0)
|
||||||
|
|
||||||
@ -199,22 +124,9 @@ object LabTrajectoryModel {
|
|||||||
val firstLabMs = anchors.first().first
|
val firstLabMs = anchors.first().first
|
||||||
val lastLabMs = anchors.last().first
|
val lastLabMs = anchors.last().first
|
||||||
|
|
||||||
// ── 2) Fenêtre effective ─────────────────────────────────────────────
|
// ── 2) Fenêtre effective = [1er ; dernier lab] ∩ fenêtre demandée ────
|
||||||
// v1.5.0 : [1er lab ; dernier lab] ∩ fenêtre demandée.
|
|
||||||
// v1.6.0 (extendBeyondLastLab) : la borne droite devient le min de la
|
|
||||||
// fenêtre demandée et de l'HORIZON DE PROLONGATION (dernière dose E2 +
|
|
||||||
// cutoff de son traitement). Au-delà du dernier lab, ratioAt() retourne
|
|
||||||
// déjà ρ_last (garde défensive aux bornes) : la boucle de calcul est
|
|
||||||
// INCHANGÉE — élargir la fenêtre EST la prolongation, et la continuité
|
|
||||||
// au point de suture est garantie par construction (ρ(t_last) = ρ_last
|
|
||||||
// exactement, dérivable à gauche comme à droite puisque ρ est constant
|
|
||||||
// après).
|
|
||||||
val t0 = maxOf(startMs, firstLabMs)
|
val t0 = maxOf(startMs, firstLabMs)
|
||||||
val t1 = if (extendBeyondLastLab) {
|
val t1 = minOf(endMs, lastLabMs)
|
||||||
minOf(endMs, extensionHorizonEndMs(estrogenTreatments, doseLogs, lastLabMs) ?: lastLabMs)
|
|
||||||
} else {
|
|
||||||
minOf(endMs, lastLabMs)
|
|
||||||
}
|
|
||||||
if (t1 <= t0) return AnchoredCurve(emptyList(), anchors.size)
|
if (t1 <= t0) return AnchoredCurve(emptyList(), anchors.size)
|
||||||
|
|
||||||
// ── 3) Courbe = M(t) × ρ(t) sur la grille ─────────────────────────────
|
// ── 3) Courbe = M(t) × ρ(t) sur la grille ─────────────────────────────
|
||||||
@ -226,34 +138,7 @@ object LabTrajectoryModel {
|
|||||||
points.add(LevelPoint(t, m * rho, 0.0)) // t=0 : pas de courbe T pour cette série
|
points.add(LevelPoint(t, m * rho, 0.0)) // t=0 : pas de courbe T pour cette série
|
||||||
t += stepMs
|
t += stepMs
|
||||||
}
|
}
|
||||||
return AnchoredCurve(points, anchors.size, lastLabMs)
|
return AnchoredCurve(points, anchors.size)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BORNE DE PROLONGATION (v1.6.0) : timestamp jusqu'où la partie extrapolée
|
|
||||||
* a un sens. Défini par le MODÈLE, pas par un cap arbitraire :
|
|
||||||
*
|
|
||||||
* horizon = (timestamp de la dernière dose E2) + cutoffHours(son traitement)
|
|
||||||
*
|
|
||||||
* où cutoffHours = 10 t½ terminales (V3C/WHS), fin de table (ODS) ou
|
|
||||||
* 30 t½ (Bateman) — cf [PharmacokineticEngine.cutoffHours]. Après cette
|
|
||||||
* borne, M(t) = 0 : la courbe prolongée serait une ligne à zéro inventée.
|
|
||||||
*
|
|
||||||
* @return le timestamp de fin de prolongation, ou `null` quand il n'y a
|
|
||||||
* RIEN à prolonger : aucune dose E2, ou modèle déjà éteint au dernier
|
|
||||||
* lab (le caller retombe alors sur la fenêtre v1.5.0).
|
|
||||||
*/
|
|
||||||
internal fun extensionHorizonEndMs(
|
|
||||||
estrogenTreatments: List<Treatment>,
|
|
||||||
doseLogs: List<DoseLog>,
|
|
||||||
lastLabMs: Long
|
|
||||||
): Long? {
|
|
||||||
val e2Doses = doseLogs.filter { d -> estrogenTreatments.any { it.id == d.treatmentId } }
|
|
||||||
val lastDose = e2Doses.maxByOrNull { it.timestamp } ?: return null
|
|
||||||
val tr = estrogenTreatments.first { it.id == lastDose.treatmentId }
|
|
||||||
val cutoffMs = (PharmacokineticEngine.cutoffHours(tr) * 3_600_000.0).toLong()
|
|
||||||
val horizonEnd = lastDose.timestamp + cutoffMs
|
|
||||||
return if (horizonEnd > lastLabMs) horizonEnd else null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -266,14 +151,10 @@ object LabTrajectoryModel {
|
|||||||
* multiplicative (pas de saut), monotone entre deux références.
|
* multiplicative (pas de saut), monotone entre deux références.
|
||||||
* Doublons de timestamp (2 labs au même instant) : le 2ᵉ ratio gagne
|
* Doublons de timestamp (2 labs au même instant) : le 2ᵉ ratio gagne
|
||||||
* (garde division par zéro).
|
* (garde division par zéro).
|
||||||
*
|
|
||||||
* HORS bornes : avant le 1ᵉʳ / après le DERNIER ancre → ρ de borne.
|
|
||||||
* v1.6.0 : la branche « après le dernier » (return ρ_last) est devenue un
|
|
||||||
* COMPORTEMENT, pas une simple garde — c'est elle qui fonde la
|
|
||||||
* prolongation (ρ constant au-delà du dernier lab, cf KDoc de l'objet) ;
|
|
||||||
* en v1.5.0 la fenêtre du caller n'y entrait jamais.
|
|
||||||
*/
|
*/
|
||||||
internal fun ratioAt(anchors: List<Pair<Long, Double>>, tMs: Long): Double {
|
internal fun ratioAt(anchors: List<Pair<Long, Double>>, tMs: Long): Double {
|
||||||
|
// Avant le 1er / après le dernier : exclus par la fenêtre du caller —
|
||||||
|
// garde défensive = bornes.
|
||||||
if (tMs <= anchors.first().first) return anchors.first().second
|
if (tMs <= anchors.first().first) return anchors.first().second
|
||||||
if (tMs >= anchors.last().first) return anchors.last().second
|
if (tMs >= anchors.last().first) return anchors.last().second
|
||||||
|
|
||||||
|
|||||||
@ -114,20 +114,7 @@ object PharmacokineticEngine {
|
|||||||
return max(0.0, c)
|
return max(0.0, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun cutoffHours(treatment: Treatment): Double {
|
||||||
* Coupure temporelle (HEURES) au-delà de laquelle une dose de ce traitement
|
|
||||||
* ne contribue plus à la prédiction :
|
|
||||||
* - profil avec modèle V3C/WHS → 10 demi-vies TERMINALES du fit
|
|
||||||
* (ex. EEn TFS ≈ 45 j, PEP ≈ 284 j) ;
|
|
||||||
* - profil tables ODS (Estrannaise) → longueur de la table (8001 h ≈ 333 j) ;
|
|
||||||
* - Bateman (gel/patch/oral) → 30 demi-vies d'élimination.
|
|
||||||
*
|
|
||||||
* Public (v1.6.0) : réutilisé par [LabTrajectoryModel.extensionHorizonEndMs]
|
|
||||||
* pour borner la PROLONGATION du tracé labs — la partie extrapolée s'éteint
|
|
||||||
* exactement quand le modèle lui-même s'éteint (même sémantique de coupure,
|
|
||||||
* une seule source de vérité).
|
|
||||||
*/
|
|
||||||
fun cutoffHours(treatment: Treatment): Double {
|
|
||||||
val profileH = if (treatment.usesProfileModel) {
|
val profileH = if (treatment.usesProfileModel) {
|
||||||
val model = treatment.pkModel
|
val model = treatment.pkModel
|
||||||
when {
|
when {
|
||||||
|
|||||||
@ -118,12 +118,6 @@ fun ChartScreen() {
|
|||||||
// revendique « passer par tes labs » et doit rester une référence de
|
// revendique « passer par tes labs » et doit rester une référence de
|
||||||
// comparaison, pas un affichage par défiant
|
// comparaison, pas un affichage par défiant
|
||||||
var showLabTrack by remember { mutableStateOf(false) }
|
var showLabTrack by remember { mutableStateOf(false) }
|
||||||
// v1.6.0 : PROLONGATION du tracé labs au-delà du dernier lab (ρ du
|
|
||||||
// dernier lab constant × modèle, bornée par l'extinction du modèle —
|
|
||||||
// cf LabTrajectoryModel.extensionHorizonEndMs). OFF par défaut : la
|
|
||||||
// partie prolongée est une ESTIMATION non ancrée, elle doit être
|
|
||||||
// demandée explicitement ; le chip n'est actif que si Tracé labs est ON.
|
|
||||||
var showLabTrackExtend by remember { mutableStateOf(false) }
|
|
||||||
// v1.4.7 (fix #58) : au CHARGEMENT des traitements, les toggles sont
|
// v1.4.7 (fix #58) : au CHARGEMENT des traitements, les toggles sont
|
||||||
// alignés sur les modèles réellement UTILISÉS par des traitements à
|
// alignés sur les modèles réellement UTILISÉS par des traitements à
|
||||||
// PROFIL PK (injection + ester, `usesProfileModel`) — en cours ou passés
|
// PROFIL PK (injection + ester, `usesProfileModel`) — en cours ou passés
|
||||||
@ -264,12 +258,11 @@ 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") —
|
||||||
// v1.5.0 : la 4ᵉ série possible est "LAB" (Tracé labs, indépendante) ;
|
// v1.5.0 : la 4ᵉ série possible est "LAB" (Tracé labs, indépendante)
|
||||||
// v1.6.0 : la 5ᵉ est "LABX" (partie PROLONGÉE du tracé labs, ρ constant)
|
|
||||||
val curves by produceState(
|
val curves by produceState(
|
||||||
emptyList<Pair<String, List<LevelPoint>>>(),
|
emptyList<Pair<String, List<LevelPoint>>>(),
|
||||||
autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast,
|
autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast,
|
||||||
showLabTrack, showLabTrackExtend, labResults.size
|
showLabTrack, labResults.size
|
||||||
) {
|
) {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
val list = mutableListOf<Pair<String, List<LevelPoint>>>()
|
||||||
@ -278,32 +271,14 @@ fun ChartScreen() {
|
|||||||
// modèle STOCKÉ des traitements — sémantique Home, cf doc §7.10)
|
// modèle STOCKÉ des traitements — sémantique Home, cf doc §7.10)
|
||||||
// et de la calibration (elle EST sa propre calibration continue —
|
// et de la calibration (elle EST sa propre calibration continue —
|
||||||
// scalePerEster ignoré, épinglé par LabTrajectoryModelTest).
|
// scalePerEster ignoré, épinglé par LabTrajectoryModelTest).
|
||||||
// showLabTrack + série visible UNIQUEMENT si ≥ 2 labs qui ancrent.
|
// showLabTrack + serie visible UNIQUEMENT si ≥ 2 labs that anchor.
|
||||||
//
|
|
||||||
// ── v1.6.0 : split ancré / prolongé ──────────────────────────────
|
|
||||||
// Avec « Prolonger », la série est SPLITTÉE au dernier lab
|
|
||||||
// significatif (lastAnchorMs) : "LAB" = partie ancrée (passe par
|
|
||||||
// les labs), "LABX" = partie prolongée (M(t) × ρ_last — une
|
|
||||||
// estimation). Deux traits distincts (LABX en atténué) préservent
|
|
||||||
// l'honnêteté de l'affichage : la partie inventée ne peut pas se
|
|
||||||
// confondre avec la partie mesurée. ⚠️ Deux clés DISTINCTES et
|
|
||||||
// non vides seulement — la légende (plus bas) doit connaître
|
|
||||||
// "LABX" AVANT son else (leçon #63).
|
|
||||||
if (showLabTrack) {
|
if (showLabTrack) {
|
||||||
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
val step = com.hormonetrack.ui.components.stepForRange(rangeHours)
|
||||||
val labCurve = LabTrajectoryModel.computeLabAnchoredCurve(
|
list.add(
|
||||||
treatments, doseLogs, labResults, startMs, endMs, step,
|
"LAB" to LabTrajectoryModel.computeLabAnchoredCurve(
|
||||||
extendBeyondLastLab = showLabTrackExtend
|
treatments, doseLogs, labResults, startMs, endMs, step
|
||||||
|
).points
|
||||||
)
|
)
|
||||||
val lastAnchor = labCurve.lastAnchorMs
|
|
||||||
if (showLabTrackExtend && lastAnchor != null) {
|
|
||||||
val anchored = labCurve.points.filter { it.timestamp <= lastAnchor }
|
|
||||||
val extended = labCurve.points.filter { it.timestamp > lastAnchor }
|
|
||||||
list.add("LAB" to anchored)
|
|
||||||
if (extended.isNotEmpty()) list.add("LABX" to extended)
|
|
||||||
} else {
|
|
||||||
list.add("LAB" to labCurve.points)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (showEse || showTfs || showWhs) {
|
if (showEse || showTfs || showWhs) {
|
||||||
// Calibration PAR MODÈLE (fix #60) : chaque courbe reçoit les
|
// Calibration PAR MODÈLE (fix #60) : chaque courbe reçoit les
|
||||||
@ -384,17 +359,6 @@ fun ChartScreen() {
|
|||||||
// et hors pics/creux (labIsSignificant a déjà filtré, §7.10)
|
// et hors pics/creux (labIsSignificant a déjà filtré, §7.10)
|
||||||
"LAB" -> ChartSeries(points, CurveStyle(ChartLabTrajectory, dashed = true), null,
|
"LAB" -> ChartSeries(points, CurveStyle(ChartLabTrajectory, dashed = true), null,
|
||||||
showExtrema = false)
|
showExtrema = false)
|
||||||
// v1.6.0 : partie PROLONGÉE du tracé labs — même couleur ATTÉNUÉE
|
|
||||||
// (alpha 0,55) pour distinguer visuellement l'estimation non
|
|
||||||
// ancrée de la partie qui passe par les labs. Même exclusions
|
|
||||||
// (pas de T, pas d'extrema — les extrema de l'interp ρ constant
|
|
||||||
// ne sont que ceux du modèle brut).
|
|
||||||
"LABX" -> ChartSeries(
|
|
||||||
points,
|
|
||||||
CurveStyle(ChartLabTrajectory.copy(alpha = 0.55f), dashed = true),
|
|
||||||
null,
|
|
||||||
showExtrema = false
|
|
||||||
)
|
|
||||||
else -> ChartSeries(points, CurveStyle(TealTertiary), CurveStyle(ChartT, dashed = true))
|
else -> ChartSeries(points, CurveStyle(TealTertiary), CurveStyle(ChartT, dashed = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -520,16 +484,6 @@ fun ChartScreen() {
|
|||||||
onClick = { showLabTrack = !showLabTrack },
|
onClick = { showLabTrack = !showLabTrack },
|
||||||
label = { Text(stringResource(R.string.show_lab_track)) }
|
label = { Text(stringResource(R.string.show_lab_track)) }
|
||||||
)
|
)
|
||||||
// v1.6.0 : prolongation du tracé labs au-delà du dernier lab
|
|
||||||
// (off par défaut, sans effet — donc désactivé — si Tracé labs
|
|
||||||
// est off : la prolongation n'existe que comme extension de CETTE
|
|
||||||
// courbe, pas comme série autonome)
|
|
||||||
FilterChip(
|
|
||||||
selected = showLabTrackExtend,
|
|
||||||
onClick = { showLabTrackExtend = !showLabTrackExtend },
|
|
||||||
enabled = showLabTrack,
|
|
||||||
label = { Text(stringResource(R.string.show_lab_track_extend)) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
@ -655,11 +609,6 @@ fun ChartScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"LAB" -> { /* légende dédiée, cf plus bas */ }
|
"LAB" -> { /* légende dédiée, cf plus bas */ }
|
||||||
// v1.6.0 : idem — la partie prolongée a sa légende
|
|
||||||
// dédiée plus bas. ⚠️ branche EXPLICITE requise :
|
|
||||||
// sinon le `else` (= TFS) imprimerait la légende
|
|
||||||
// TFS une 2ᵉ fois (leçon #63, v1.5.0)
|
|
||||||
"LABX" -> { /* légende dédiée, cf plus bas */ }
|
|
||||||
else -> {
|
else -> {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.legend_tfs_e2),
|
stringResource(R.string.legend_tfs_e2),
|
||||||
@ -706,30 +655,6 @@ fun ChartScreen() {
|
|||||||
color = ChartLabTrajectory
|
color = ChartLabTrajectory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// v1.6.0 : légende de la partie PROLONGÉE + AVERTISSEMENT —
|
|
||||||
// seulement si la série "LABX" existe (chip Prolonger actif
|
|
||||||
// ET horizon de prolongation atteint dans la fenêtre visible)
|
|
||||||
val labExtendVisible = showLabTrack && showLabTrackExtend &&
|
|
||||||
curves.any { it.first == "LABX" && it.second.size >= 2 }
|
|
||||||
if (labExtendVisible) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.legend_lab_track_extend),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = ChartLabTrajectory.copy(alpha = 0.55f)
|
|
||||||
)
|
|
||||||
// Avertissement (demande v1.6.0) : la partie prolongée
|
|
||||||
// est une EXTRAPOLATION sans garantie — elle repose sur
|
|
||||||
// des mesures de labs elles-mêmes faillibles. Affiché
|
|
||||||
// TANT QUE la prolongation est visible (sous la
|
|
||||||
// légende), pour qu'elle ne puisse pas être lue comme
|
|
||||||
// une prédiction fiable de l'avenir.
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.lab_track_extend_warning),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.drag_hint),
|
stringResource(R.string.drag_hint),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
|||||||
@ -33,9 +33,6 @@
|
|||||||
<string name="show_forecast">Prévision</string>
|
<string name="show_forecast">Prévision</string>
|
||||||
<string name="show_extrema">Pics / creux</string>
|
<string name="show_extrema">Pics / creux</string>
|
||||||
<string name="show_lab_track">Tracé labs</string>
|
<string name="show_lab_track">Tracé labs</string>
|
||||||
<!-- v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in,
|
|
||||||
actif seulement quand Tracé labs est ON) -->
|
|
||||||
<string name="show_lab_track_extend">Prolonger</string>
|
|
||||||
<string name="zoom_in">Zoom avant (fenêtre plus courte)</string>
|
<string name="zoom_in">Zoom avant (fenêtre plus courte)</string>
|
||||||
<string name="zoom_out">Zoom arrière (fenêtre plus longue)</string>
|
<string name="zoom_out">Zoom arrière (fenêtre plus longue)</string>
|
||||||
<string name="hrt_since">Sous THS depuis le %1$s</string>
|
<string name="hrt_since">Sous THS depuis le %1$s</string>
|
||||||
@ -77,12 +74,6 @@
|
|||||||
<string name="agenda_permission">L\'autorisation agenda est demandée à l\'activation.</string>
|
<string name="agenda_permission">L\'autorisation agenda est demandée à l\'activation.</string>
|
||||||
<string name="legend_extrema">▲▼ pics & creux estimés (par courbe)</string>
|
<string name="legend_extrema">▲▼ pics & creux estimés (par courbe)</string>
|
||||||
<string name="legend_lab_track">E2 ancrée sur tes labs — forme du modèle forcée de passer par chaque prise de sang (entre le 1er et le dernier lab)</string>
|
<string name="legend_lab_track">E2 ancrée sur tes labs — forme du modèle forcée de passer par chaque prise de sang (entre le 1er et le dernier lab)</string>
|
||||||
<!-- v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX") -->
|
|
||||||
<string name="legend_lab_track_extend">-- Tracé labs prolongé après ton dernier lab (modèle × ratio du dernier lab — estimation, plus ancrée)</string>
|
|
||||||
<!-- v1.6.0 : AVERTISSEMENT affiché sous la légende quand la prolongation
|
|
||||||
est active — l\'extrapolation n\'a aucune garantie et repose sur des
|
|
||||||
mesures elles-mêmes faillibles -->
|
|
||||||
<string name="lab_track_extend_warning">⚠ Simple simulation, sans garantie de correspondre au réel : la prolongation extrapole ton modèle à partir de tes résultats de laboratoire — qui peuvent eux-mêmes être erronés. Fie-toi à ta prochaine prise de sang, pas à cette courbe.</string>
|
|
||||||
<string name="legend_forecast_dose">— prochaine dose projetée (marqueur vertical)</string>
|
<string name="legend_forecast_dose">— prochaine dose projetée (marqueur vertical)</string>
|
||||||
<string name="back_to_present">Revenir à maintenant</string>
|
<string name="back_to_present">Revenir à maintenant</string>
|
||||||
<string name="drag_hint">Fais glisser le graphique vers la droite pour remonter dans le passé.</string>
|
<string name="drag_hint">Fais glisser le graphique vers la droite pour remonter dans le passé.</string>
|
||||||
|
|||||||
@ -33,9 +33,6 @@
|
|||||||
<string name="show_forecast">Forecast</string>
|
<string name="show_forecast">Forecast</string>
|
||||||
<string name="show_extrema">Peaks / troughs</string>
|
<string name="show_extrema">Peaks / troughs</string>
|
||||||
<string name="show_lab_track">Lab track</string>
|
<string name="show_lab_track">Lab track</string>
|
||||||
<!-- v1.6.0 : prolongation du tracé labs au-delà du dernier lab (opt-in,
|
|
||||||
actif seulement quand Lab track est ON) -->
|
|
||||||
<string name="show_lab_track_extend">Extend</string>
|
|
||||||
<string name="zoom_in">Zoom in (shorter window)</string>
|
<string name="zoom_in">Zoom in (shorter window)</string>
|
||||||
<string name="zoom_out">Zoom out (longer window)</string>
|
<string name="zoom_out">Zoom out (longer window)</string>
|
||||||
<string name="hrt_since">Under HRT since %1$s</string>
|
<string name="hrt_since">Under HRT since %1$s</string>
|
||||||
@ -77,12 +74,6 @@
|
|||||||
<string name="agenda_permission">Calendar permission is requested when you enable this.</string>
|
<string name="agenda_permission">Calendar permission is requested when you enable this.</string>
|
||||||
<string name="legend_extrema">▲▼ estimated peaks & troughs (per curve)</string>
|
<string name="legend_extrema">▲▼ estimated peaks & troughs (per curve)</string>
|
||||||
<string name="legend_lab_track">Lab-anchored curve — model shape forced through your labs (E2 only, between first and last lab)</string>
|
<string name="legend_lab_track">Lab-anchored curve — model shape forced through your labs (E2 only, between first and last lab)</string>
|
||||||
<!-- v1.6.0 : légende de la partie PROLONGÉE du tracé labs (série "LABX") -->
|
|
||||||
<string name="legend_lab_track_extend">-- Lab track extended past your last lab (model × last lab ratio — estimate, not anchored)</string>
|
|
||||||
<!-- v1.6.0 : AVERTISSEMENT affiché sous la légende quand la prolongation
|
|
||||||
est active — l\'extrapolation n\'a aucune garantie et repose sur des
|
|
||||||
mesures elles-mêmes faillibles -->
|
|
||||||
<string name="lab_track_extend_warning">⚠ Simulation only, with no guarantee of matching reality: the extension extrapolates your model from your lab results — which can themselves be inaccurate. Trust your next blood test, not this curve.</string>
|
|
||||||
<string name="legend_forecast_dose">— next projected dose (vertical marker)</string>
|
<string name="legend_forecast_dose">— next projected dose (vertical marker)</string>
|
||||||
<string name="back_to_present">Back to now</string>
|
<string name="back_to_present">Back to now</string>
|
||||||
<string name="drag_hint">Drag the chart to the right to travel into the past.</string>
|
<string name="drag_hint">Drag the chart to the right to travel into the past.</string>
|
||||||
|
|||||||
@ -14,14 +14,12 @@ import kotlin.math.ln
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests du « Tracé labs » (v1.5.0) — courbe hybride ancrée sur les labs
|
* Tests du « Tracé labs » (v1.5.0) — courbe hybride ancrée sur les labs
|
||||||
* (cf doc §7.10), et de sa PROLONGATION au-delà du dernier lab (v1.6.0,
|
* (cf doc §7.10). Chaque test épingle une GARANTIE de la construction :
|
||||||
* cf doc §7.10.bis). Chaque test épingle une GARANTIE de la construction :
|
|
||||||
* - passage EXACT sur chaque lab (comme les sanity checks TFS) ;
|
* - passage EXACT sur chaque lab (comme les sanity checks TFS) ;
|
||||||
* - interp log-linéaire de ρ (math exacte, tolérance FP seule) ;
|
* - interp log-linéaire de ρ (math exacte, tolérance FP seule) ;
|
||||||
* - garde de significativité jamais contournée (leçon #61 : une dose de
|
* - garde de significativité jamais contournée (leçon #61 : une dose de
|
||||||
* test ancienne + labs tardifs ne doivent pas créer de zigzag) ;
|
* test ancienne + labs tardifs ne doivent pas créer de zigzag) ;
|
||||||
* - fenêtre = [1er ; dernier lab] par défaut ; avec l'option v1.6.0,
|
* - fenêtre = [1er ; dernier lab] — JAMAIS d'extrapolation ;
|
||||||
* prolongation ρ constant jusqu'au cutoff du modèle (JAMAIS au-delà) ;
|
|
||||||
* - indépendance du scaleFactor stocké (cette courbe est SA PROPRE
|
* - indépendance du scaleFactor stocké (cette courbe est SA PROPRE
|
||||||
* calibration continue — combiner = double correction).
|
* calibration continue — combiner = double correction).
|
||||||
*
|
*
|
||||||
@ -263,188 +261,4 @@ class LabTrajectoryModelTest {
|
|||||||
assertEquals(1.0, LabTrajectoryModel.ratioAt(anchors, -5L), 1e-12)
|
assertEquals(1.0, LabTrajectoryModel.ratioAt(anchors, -5L), 1e-12)
|
||||||
assertEquals(2.0, LabTrajectoryModel.ratioAt(anchors, 100L), 1e-12)
|
assertEquals(2.0, LabTrajectoryModel.ratioAt(anchors, 100L), 1e-12)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Prolongation au-delà du dernier lab (v1.6.0) ─────────────────────────
|
|
||||||
// Garanties de l'extension (cf doc §7.10.bis) :
|
|
||||||
// - au-delà du dernier lab : courbe(t) = M(t) × ρ_last (ρ CONSTANT —
|
|
||||||
// fondement = la garde défensive de ratioAt devenue comportement) ;
|
|
||||||
// - continuité au point de suture (le dernier lab reste exact) ;
|
|
||||||
// - horizon = dernière dose E2 + cutoff de son traitement (jamais de
|
|
||||||
// ligne à zéro inventée au-delà de l'extinction du modèle) ;
|
|
||||||
// - les doses loguées APRÈS le dernier lab (autre ester/dose compris)
|
|
||||||
// refont monter la courbe via M(t) — la demande « dosage + type
|
|
||||||
// d'ester + évolution classique » ;
|
|
||||||
// - dernier lab non significatif : ni ancre ni point de départ de la
|
|
||||||
// prolongation (garde #61 inchangée) ;
|
|
||||||
// - flag OFF (défaut) = comportement v1.5.0 bit-compatible.
|
|
||||||
|
|
||||||
/** Labs DATA-DRIVEN ancrés à 2 j (ρ=0,9) et 7 j (ρ=1,2) sur la fixture EEn/TFS. */
|
|
||||||
private fun anchoredLabs(tr: Treatment, doses: List<com.hormonetrack.data.model.DoseLog>):
|
|
||||||
Pair<List<LabResult>, Double> {
|
|
||||||
val m2 = PharmacokineticEngine.e2At(listOf(tr), doses, 2 * DAY_MS, null, null)
|
|
||||||
val m7 = PharmacokineticEngine.e2At(listOf(tr), doses, 7 * DAY_MS, null, null)
|
|
||||||
return listOf(lab(0.9 * m2, 2 * DAY_MS), lab(1.2 * m7, 7 * DAY_MS)) to 1.2
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension - la courbe continue apres le dernier lab avec le rho du dernier lab`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val doses = doses(tr)
|
|
||||||
val (labs, rhoLast) = anchoredLabs(tr, doses)
|
|
||||||
|
|
||||||
val curve = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses, labs, 0L, 60 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = true
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue("la prolongation produit des points après le dernier lab",
|
|
||||||
curve.points.any { it.timestamp > 7 * DAY_MS })
|
|
||||||
// Suture : le dernier lab reste EXACT (continuité par construction)
|
|
||||||
assertEquals(labs[1].value, curve.points.first { it.timestamp == 7 * DAY_MS }.e2, 1e-6)
|
|
||||||
// Identité de la zone extrapolée : courbe(t) = M(t) × ρ_last, exact
|
|
||||||
for (day in listOf(10L, 20L, 30L)) {
|
|
||||||
val t = day * DAY_MS
|
|
||||||
val p = curve.points.first { it.timestamp == t }
|
|
||||||
val m = PharmacokineticEngine.e2At(listOf(tr.copy(scaleFactor = 1.0)), doses, t, null, null)
|
|
||||||
assertEquals("courbe($day j) == M × ρ_last", m * rhoLast, p.e2, 1e-9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension - horizon borne par le cutoff de la derniere dose`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val (labs, _) = anchoredLabs(tr, doses(tr))
|
|
||||||
|
|
||||||
// Demande TRÈS large (90 j) : la prolongation s'arrête au modèle
|
|
||||||
val curve = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), labs, 0L, 90 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = true
|
|
||||||
)
|
|
||||||
|
|
||||||
val horizonEnd = LabTrajectoryModel.extensionHorizonEndMs(
|
|
||||||
listOf(tr), doses(tr), 7 * DAY_MS)!!
|
|
||||||
assertTrue("l'horizon EEn/TFS dépasse le dernier lab (sinon le test ne prouve rien)",
|
|
||||||
horizonEnd > 7 * DAY_MS)
|
|
||||||
assertTrue("aucun point au-delà de l'extinction du modèle",
|
|
||||||
curve.points.all { it.timestamp <= horizonEnd })
|
|
||||||
assertTrue("le dernier point s'arrête AU cutoff (pas au-delà d'un pas)",
|
|
||||||
curve.points.last().timestamp <= horizonEnd)
|
|
||||||
assertTrue(curve.points.last().timestamp + HOUR_MS > horizonEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension - une dose apres le dernier lab refait monter la courbe (dose et ester)`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val doses = doses(tr) + com.hormonetrack.data.model.DoseLog(
|
|
||||||
id = 2, treatmentId = 1, timestamp = 10 * DAY_MS,
|
|
||||||
doseAmount = 4.0, esterType = "EV" // override d'ester : EEn → EV
|
|
||||||
)
|
|
||||||
val (labs, rhoLast) = anchoredLabs(tr, doses(tr))
|
|
||||||
|
|
||||||
val curve = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses, labs, 0L, 20 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = true
|
|
||||||
)
|
|
||||||
val extension = curve.points.filter { it.timestamp > 7 * DAY_MS }
|
|
||||||
assertTrue(extension.isNotEmpty())
|
|
||||||
|
|
||||||
// L'identité vaut AUSSI avec la dose EV : M(t) inclut les deux doses
|
|
||||||
val t12 = 12 * DAY_MS
|
|
||||||
val m12 = PharmacokineticEngine.e2At(listOf(tr.copy(scaleFactor = 1.0)), doses, t12, null, null)
|
|
||||||
assertEquals(m12 * rhoLast, extension.first { it.timestamp == t12 }.e2, 1e-9)
|
|
||||||
|
|
||||||
// La FORME suit l'injection EV : montée nette après le 10 j (Tmax EV
|
|
||||||
// ~45 h) — la partie prolongée n'est PAS une simple décroissance EEn
|
|
||||||
val at8 = extension.first { it.timestamp == 8 * DAY_MS }.e2
|
|
||||||
val peak = extension.filter { it.timestamp in 10 * DAY_MS..14 * DAY_MS }.maxOf { it.e2 }
|
|
||||||
assertTrue("le pic EV ($peak) doit dépasser le niveau pré-injection ($at8)",
|
|
||||||
peak > at8 * 1.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension desactivee - rien apres le dernier lab (comportement initial v1-5-0)`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val (labs, _) = anchoredLabs(tr, doses(tr))
|
|
||||||
// DÉFAUT (flag absent) ET flag explicite à false : identiques
|
|
||||||
val byDefault = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), labs, 0L, 60 * DAY_MS, HOUR_MS)
|
|
||||||
val explicitOff = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), labs, 0L, 60 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = false)
|
|
||||||
assertEquals(byDefault.points, explicitOff.points)
|
|
||||||
assertTrue(byDefault.points.all { it.timestamp <= 7 * DAY_MS })
|
|
||||||
assertTrue("lastAnchorMs est positionné même sans prolongation (l'UI peut splitter)",
|
|
||||||
byDefault.lastAnchorMs == 7 * DAY_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension - un lab non significatif ne prolonge pas (garde 61)`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
// Scénario #61 : lab TARDIF (60 j — prédiction résiduelle 0, ratio aberrant)
|
|
||||||
val labs = anchoredLabs(tr, doses(tr)).first + lab(3000.0, 60 * DAY_MS)
|
|
||||||
|
|
||||||
val curve = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), labs, 0L, 90 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = true
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue("le lab tardif reste rejeté", curve.anchoredLabs == 2)
|
|
||||||
assertEquals("la prolongation part du dernier lab SIGNIFICATIF",
|
|
||||||
7 * DAY_MS, curve.lastAnchorMs)
|
|
||||||
assertTrue("des points prolongés existent bien après le dernier ancre",
|
|
||||||
curve.points.any { it.timestamp > 7 * DAY_MS })
|
|
||||||
assertTrue("aucun point ne suit la valeur absurde du lab rejeté",
|
|
||||||
curve.points.none { it.e2 > 600.0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extension - demande finissant avant le dernier lab ne prolonge rien`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val (labs, _) = anchoredLabs(tr, doses(tr))
|
|
||||||
val curve = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), labs, 0L, 5 * DAY_MS, HOUR_MS,
|
|
||||||
extendBeyondLastLab = true
|
|
||||||
)
|
|
||||||
assertTrue(curve.points.all { it.timestamp <= 5 * DAY_MS })
|
|
||||||
// la suture au dernier ancre n'est pas dans la fenêtre → aucun point
|
|
||||||
// au-delà du dernier lab, comme en v1.5.0
|
|
||||||
assertTrue(curve.points.none { it.timestamp > 7 * DAY_MS })
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `extensionHorizonEndMs - gardes aucune dose ou modele deja eteint`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val trs = listOf(tr)
|
|
||||||
// Aucune dose E2 → rien à prolonger
|
|
||||||
assertTrue(LabTrajectoryModel.extensionHorizonEndMs(trs, emptyList(), 7 * DAY_MS) == null)
|
|
||||||
// Dose à t=0 → horizon = cutoff du traitement (EEn TFS : 10 t½ term.)
|
|
||||||
val horizon = LabTrajectoryModel.extensionHorizonEndMs(trs, doses(tr), 7 * DAY_MS)
|
|
||||||
val expected = (PharmacokineticEngine.cutoffHours(tr) * 3_600_000.0).toLong()
|
|
||||||
assertEquals(expected, horizon)
|
|
||||||
// Modèle DÉJÀ éteint au dernier lab → null (retour fenêtre v1.5.0)
|
|
||||||
assertTrue(LabTrajectoryModel.extensionHorizonEndMs(
|
|
||||||
trs, doses(tr), expected + HOUR_MS) == null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `lastAnchorMs - null si moins de 2 ancres, positionne sinon`() {
|
|
||||||
initProfiles()
|
|
||||||
val tr = treatment()
|
|
||||||
val one = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), listOf(lab(200.0, 3 * DAY_MS)), 0L, 30 * DAY_MS, HOUR_MS)
|
|
||||||
assertTrue(one.points.isEmpty())
|
|
||||||
assertTrue(one.lastAnchorMs == null)
|
|
||||||
|
|
||||||
val two = LabTrajectoryModel.computeLabAnchoredCurve(
|
|
||||||
listOf(tr), doses(tr), listOf(lab(300.0, 2 * DAY_MS), lab(260.0, 7 * DAY_MS)),
|
|
||||||
0L, 30 * DAY_MS, HOUR_MS)
|
|
||||||
assertEquals(7 * DAY_MS, two.lastAnchorMs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,49 +3,6 @@
|
|||||||
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.10`, …).
|
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
|
||||||
|
|
||||||
## [1.6.0] — 2026-09-12 (versionCode 32)
|
|
||||||
|
|
||||||
### « Tracé labs » prolongé au-delà du dernier lab
|
|
||||||
|
|
||||||
- **Nouveau chip `Prolonger`** (à côté de `Tracé labs`, **off par défaut**,
|
|
||||||
sans effet — donc désactivé — tant que Tracé labs est off) : la courbe
|
|
||||||
ancrée sur tes prises de sang se PROLONGE désormais au-delà du dernier lab,
|
|
||||||
au lieu de s'arrêter là (limite v1.5.0 : justement la période la plus
|
|
||||||
récente était invisible).
|
|
||||||
- **Recette de la prolongation** : `courbe(t) = M(t) × ρ_dernier lab` où
|
|
||||||
M(t) = le modèle PK brut (porte TOUTE la dynamique : Tmax, queues,
|
|
||||||
accumulation **et les doses loguées après le dernier lab** — une nouvelle
|
|
||||||
injection, même à un autre ester, fait repartir la courbe en pic) et
|
|
||||||
ρ = le ratio corps-réel ÷ modèle observé au dernier lab, supposé constant.
|
|
||||||
Demande couverte : simuler à partir des labs précédents, du dosage et du
|
|
||||||
type d'ester injecté, et de l'évolution classique de l'ester.
|
|
||||||
- **Horizon honnête** : la prolongation s'arrête quand le MODÈLE lui-même
|
|
||||||
s'éteint (dernière dose + coupure moteur = 10 t½ terminales V3C/WHS, fin
|
|
||||||
de table ODS, 30 t½ Bateman) — jamais de ligne à zéro inventée au-delà.
|
|
||||||
- **Lisibilité** : la partie prolongée est dessinée SÉPARÉMENT (même rose,
|
|
||||||
atténué) avec sa propre légende — « estimation, plus ancrée » : la partie
|
|
||||||
mesurée (qui passe par les labs) ne peut pas se confondre avec la partie
|
|
||||||
simulée.
|
|
||||||
- **Avertissement visible** (demande) : tant que la partie prolongée est
|
|
||||||
affichée, un texte sous la légende rappelle que c'est une **simple
|
|
||||||
simulation sans garantie de correspondre au réel**, extrapolée à partir
|
|
||||||
de tes résultats de laboratoire — qui peuvent **eux-mêmes être erronés**
|
|
||||||
— et qu'il faut se fier à la prochaine prise de sang, pas à cette courbe.
|
|
||||||
- **Garde anti-ratés inchangée** : seuls les labs SIGNIFICATIFS ancrent
|
|
||||||
(leçon #61) — un lab tardif hors fenêtre d'action ne prolonge pas la
|
|
||||||
courbe et n'est jamais traversé.
|
|
||||||
- **Limites documentées** (cf doc de développement §7.10.bis) : ρ est
|
|
||||||
constant (pas d'extrapolation de pente — elle divergerait sans base
|
|
||||||
physiologique) ; après un changement d'ester, l'amplitude reste calibrée
|
|
||||||
par le ρ de l'ancien ester (même limite que la calibration classique) ;
|
|
||||||
les doses PRÉVISIONNELLES ne participent pas (la projection reste la
|
|
||||||
série Prévision).
|
|
||||||
- **8 nouveaux tests** (191 au total / 168 sans les données locales) :
|
|
||||||
identité ρ constant + continuité de suture, horizon = cutoff du
|
|
||||||
traitement, montée après une dose EV loguée après le dernier lab,
|
|
||||||
flag off = comportement v1.5.0 bit-compatible, garde #61 sur la
|
|
||||||
prolongation, gardes du helper d'horizon, `lastAnchorMs`.
|
|
||||||
|
|
||||||
## [1.5.0] — 2026-09-11 (versionCode 31)
|
## [1.5.0] — 2026-09-11 (versionCode 31)
|
||||||
|
|
||||||
### « Tracé labs » — courbe ancrée sur tes prises de sang
|
### « Tracé labs » — courbe ancrée sur tes prises de sang
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
4. [Environnement de build (cette machine)](#4-environnement-de-build-cette-machine)
|
4. [Environnement de build (cette machine)](#4-environnement-de-build-cette-machine)
|
||||||
5. [Architecture générale](#5-architecture-générale)
|
5. [Architecture générale](#5-architecture-générale)
|
||||||
6. [Modèle de données (Room)](#6-modèle-de-données-room) + [6.bis Sémantique isActive](#6bis-sémantique-isactive-v124--drapeau-administratif-jamais-un-filtre)
|
6. [Modèle de données (Room)](#6-modèle-de-données-room) + [6.bis Sémantique isActive](#6bis-sémantique-isactive-v124--drapeau-administratif-jamais-un-filtre)
|
||||||
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique) + [7.8 Modèle TFS V3C](#78-modèle-tfs-v3c-v140--pktransfemsciencemodelskt) + [7.9 Modèle WHSAH](#79-modèle-whsah-v146--pkwhsahmodelskt) + [7.10 « Tracé labs »](#710--tracé-labs--v150--pklabtrajectorymodelkt) + [7.10.bis Prolongation](#710bis-prolongation-au-delà-du-dernier-lab-v160--pklabtrajectorymodelkt)
|
7. [Moteur pharmacocinétique](#7-moteur-pharmacocinétique) + [7.8 Modèle TFS V3C](#78-modèle-tfs-v3c-v140--pktransfemsciencemodelskt) + [7.9 Modèle WHSAH](#79-modèle-whsah-v146--pkwhsahmodelskt)
|
||||||
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test hors dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data)
|
8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test hors dépôt](#8bis-données-de-test-réelles--hors-dépôt-local-test-data)
|
||||||
9. [Système de rappels](#9-système-de-rappels) + [9.bis Alertes de seuil](#9bis-alertes-de-seuil-reminderalertnotifierkt--worker-v142)
|
9. [Système de rappels](#9-système-de-rappels) + [9.bis Alertes de seuil](#9bis-alertes-de-seuil-reminderalertnotifierkt--worker-v142)
|
||||||
10. [UI & navigation](#10-ui--navigation)
|
10. [UI & navigation](#10-ui--navigation)
|
||||||
@ -63,7 +63,6 @@ Fonctionnalités v1 :
|
|||||||
|
|
||||||
| Date | Événement |
|
| Date | Événement |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 12 sept. 2026 (session v1.6.0) | **« Tracé labs » PROLONGÉ au-delà du dernier lab** (demande) : le tracé s'arrêtait AU dernier lab (v1.5.0) — justement la période la plus récente était invisible. FIX : chip `Prolonger` (5ᵉ de la rangée, off par défaut, désactivé tant que Tracé labs est off) → au-delà du dernier lab significatif, `courbe(t) = M(t) × ρ_last` (ρ CONSTANT — pas d'extrapolation de pente, elle divergerait sans base physiologique) ; M(t) inclut AUTOMATIQUEMENT les doses loguées après le dernier lab (une injection EV après des labs EEn refait monter la courbe — épinglé par test) ; horizon = dernière dose E2 + cutoffHours de son traitement (une seule source de vérité : `cutoffHours` rendu public) ; série SPLITTÉE en "LAB" (ancré) / "LABX" (prolongé, rose atténué α 0,55 + légende dédiée — l'estimation ne se confond pas avec le mesuré) ; branche "LABX" EXPLICITE dans le when de légende AVANT le else (leçon #63) ; **AVERTISSEMENT visible sous la légende quand la prolongation est affichée** (demande) : « simple simulation, sans garantie de correspondre au réel, basée sur tes labs qui peuvent eux-mêmes être erronés » (string FR/EN `lab_track_extend_warning`). < 2 ancres, modèle déjà éteint au dernier lab, ou demande finissant avant → pas de prolongation (retour v1.5.0 exact, épinglé bit-compatible). 8 nouveaux tests (191 verts / 168 sans données locales) + lint vert. |
|
|
||||||
| 11 sept. 2026 (session v1.5.0) | **« Tracé labs »** (demande débattue) : courbe hybride `M(t)×ρ(t)` ancrée sur les labs — ρ log-linéaire entre labs, garde #61, E2 seul, fenêtre labs, chip off par défaut + scroll horizontal CONFINÉ (chip 4ᵉ coupé hors fenêtre, constat émulateur) + leçon #63 légende dupliquée ("LAB" tombant dans else TFS) ; ChartSeries.showExtrema=false pour la série ; 11 tests LabTrajectoryModelTest (183 verts) + lint ; validé émulateur (courbe passe exactement sur le lab 248, 0 crash). |
|
| 11 sept. 2026 (session v1.5.0) | **« Tracé labs »** (demande débattue) : courbe hybride `M(t)×ρ(t)` ancrée sur les labs — ρ log-linéaire entre labs, garde #61, E2 seul, fenêtre labs, chip off par défaut + scroll horizontal CONFINÉ (chip 4ᵉ coupé hors fenêtre, constat émulateur) + leçon #63 légende dupliquée ("LAB" tombant dans else TFS) ; ChartSeries.showExtrema=false pour la série ; 11 tests LabTrajectoryModelTest (183 verts) + lint ; validé émulateur (courbe passe exactement sur le lab 248, 0 crash). |
|
||||||
| 5 sept. 2026 (session 1) | Plan, vérification GT 3 = Lite Wearable, création couche données + ancien moteur Bateman + ancien ReminderManager. Extraction des modèles du `Estrogen.ods` → `/tmp/pk_models.json` (6 profils × 8001 h + params D/k1–k3). |
|
| 5 sept. 2026 (session 1) | Plan, vérification GT 3 = Lite Wearable, création couche données + ancien moteur Bateman + ancien ReminderManager. Extraction des modèles du `Estrogen.ods` → `/tmp/pk_models.json` (6 profils × 8001 h + params D/k1–k3). |
|
||||||
| 5 sept. 2026 | L'utilisatrice mentionne un travail d'un assistant tiers « **Mimo V2.5** » : **aucune trace trouvée** (fichiers identiques à la session 1, timestamps identiques). Reprise depuis l'état existant. Bugs trouvés au passage : settings.gradle, BootReceiver, cancel PendingIntent. |
|
| 5 sept. 2026 | L'utilisatrice mentionne un travail d'un assistant tiers « **Mimo V2.5** » : **aucune trace trouvée** (fichiers identiques à la session 1, timestamps identiques). Reprise depuis l'état existant. Bugs trouvés au passage : settings.gradle, BootReceiver, cancel PendingIntent. |
|
||||||
@ -486,21 +485,18 @@ MAIS passant par tous les points ».
|
|||||||
- **Garde anti-#61** : labs croissants + `labIsSignificant` — un lab hors
|
- **Garde anti-#61** : labs croissants + `labIsSignificant` — un lab hors
|
||||||
fenêtre d'action (dose de test de janvier, labs tardifs) N'EST PAS un
|
fenêtre d'action (dose de test de janvier, labs tardifs) N'EST PAS un
|
||||||
ancrage (reproduction épinglée dans LabTrajectoryModelTest).
|
ancrage (reproduction épinglée dans LabTrajectoryModelTest).
|
||||||
- **E2 uniquement** : les labs T sont trop rares + unités variées
|
- **E3 = E2 uniquement** : les labs T sont trop rares + unités variées
|
||||||
(bug #26) — hors scope (choix v1.5.0). Pas de courbe T pour la série
|
(bug #26) — hors scope (choix v1.5.0). Pas de courbe T pour la série
|
||||||
(`tStyle = null` → rien de dessiné, miroir CurveChart).
|
(`tStyle = null` → rien de dessiné, miroir CurveChart).
|
||||||
- **Fenêtre = [1er lab significatif ; dernier lab] ∩ fenêtre du graphique** —
|
- **Fenêtre = [1er lab significatif ; dernier lab] ∩ fenêtre du graphique** —
|
||||||
toujours bornée aux labs ; vieillir après le dernier lab = rien dessiné
|
toujours bornée aux labs ; vieillir après le dernier lab = rien dessiné
|
||||||
(l'app n'invente pas de niveau). ⚠️ v1.6.0 : ce comportement reste le
|
(l'app n'invente pas de niveau).
|
||||||
DÉFAUT — avec le chip « Prolonger », la courbe peut se PROLONGER au-delà
|
|
||||||
du dernier lab (ρ constant, horizon = extinction du modèle — cf
|
|
||||||
§7.10.bis).
|
|
||||||
- < 2 labs significatifs → courbe vide (un lab seul = pas d'intervalle ;
|
- < 2 labs significatifs → courbe vide (un lab seul = pas d'intervalle ;
|
||||||
un point isolé ne définit pas de tracé) — et donc jamais de prolongation.
|
un point isolé ne définit pas de tracé).
|
||||||
- **Indépendante de la calibration** : elle EST sa propre calibration
|
- **Indépendante de la calibration** : elle EST sa propre calibration
|
||||||
continue (ratio brut, `scaleFactor` stocké ignoré ET `scalePerEster`
|
continue (ratio brut, `scaleFactor` stocké ignoré ET `scalePerEster`
|
||||||
ignoré — l'inverse serait une DOUBLE correction, épinglé par test).
|
ignoré — l'inverse serait une DOUBLE correction, épinglé par test).
|
||||||
effet : les modèles rapprochés au sein du même ancrage CONVERGENT
|
efecto : les modèles rapprochés au sein du même ancrage CONVERGENT
|
||||||
(le mi-chemin ESE vs WHS colle à ~2 % — `modelOverride` garde une
|
(le mi-chemin ESE vs WHS colle à ~2 % — `modelOverride` garde une
|
||||||
empreinte intermédiaire, testé avec le seuil du test moteur WHS).
|
empreinte intermédiaire, testé avec le seuil du test moteur WHS).
|
||||||
- **Hors pics/creux** (chip "Pics / creux") : la série passe
|
- **Hors pics/creux** (chip "Pics / creux") : la série passe
|
||||||
@ -509,93 +505,16 @@ MAIS passant par tous les points ».
|
|||||||
- **UI** : chip « Tracé labs » / "Lab track" dans la RANGÉE MODÈLES (4ᵉ
|
- **UI** : chip « Tracé labs » / "Lab track" dans la RANGÉE MODÈLES (4ᵉ
|
||||||
chip — scroll horizontal CONFINÉ à la rangée ; constat v1.5.0 en émulateur :
|
chip — scroll horizontal CONFINÉ à la rangée ; constat v1.5.0 en émulateur :
|
||||||
le 4ᵉ chip était coupé hors fenêtre avec une Row figée). OFF par défaut.
|
le 4ᵉ chip était coupé hors fenêtre avec une Row figée). OFF par défaut.
|
||||||
v1.6.0 : 5ᵉ chip « Prolonger » (off par défaut, désactivé tant que
|
|
||||||
Tracé labs est off) — prolongation au-delà du dernier lab (§7.10.bis).
|
|
||||||
- ⚠️ **Leçon #63 (v1.5.0, émulateur)** : la clé "LAB" dans la liste `curves`
|
- ⚠️ **Leçon #63 (v1.5.0, émulateur)** : la clé "LAB" dans la liste `curves`
|
||||||
doit être EXCLUE de la boucle de légende (`when (model)` — le `else`
|
doit être EXCLUE de la boucle de légende (`when (model)` — le `else`
|
||||||
= TFS imprimerait la légende TFS une 2ᵉ fois). Série → couleur dédiée
|
= TFS imprimerait la légende TFS une 2ᵉ fois). Série → couleur dédiée
|
||||||
`ChartLabTrajectory` #C2185B dashed. v1.6.0 : la clé "LABX" (partie
|
`ChartLabTrajectory` #C2185B dashed.
|
||||||
prolongée) a la MÊME exigence — branche explicite dans le `when`, sinon
|
|
||||||
le `else` (= TFS) imprimerait la légende TFS une 2ᵉ fois.
|
|
||||||
- **INVARIANT** : cette courbe n'entre JAMAIS dans `levelAt` — accueil,
|
- **INVARIANT** : cette courbe n'entre JAMAIS dans `levelAt` — accueil,
|
||||||
seuils d'alerte, rappels utilisent toujours le modèle calibré (§9.bis).
|
seuils d'alerte, rappels utilisent toujours le modèle calibré (§9.bis).
|
||||||
|
|
||||||
### 7.10.bis Prolongation au-delà du dernier lab (v1.6.0) — `pk/LabTrajectoryModel.kt`
|
|
||||||
|
|
||||||
**Demande v1.6.0** : permettre au tracé labs de s'étendre au-delà du
|
|
||||||
dernier lab, en simulant à partir des labs précédents, du dosage et du
|
|
||||||
type d'ester injecté, et de l'évolution classique de l'ester. Le constat
|
|
||||||
v1.5.0 : la fenêtre s'arrêtait AU dernier lab — précisément la période la
|
|
||||||
plus récente (entre la dernière prise de sang et maintenant) était
|
|
||||||
invisible sur la courbe de comparaison.
|
|
||||||
|
|
||||||
**Recette** : `extendBeyondLastLab = true` (paramètre de
|
|
||||||
`computeLabAnchoredCurve`, chip `Prolonger` — off par défaut, désactivé
|
|
||||||
tant que Tracé labs est off). La borne droite de la fenêtre devient
|
|
||||||
`min(demande, horizon)` et **la boucle de calcul est INCHANGÉE** :
|
|
||||||
`ratioAt` retournait déjà ρ_last au-delà du dernier ancre (garde
|
|
||||||
défensive v1.5.0) — élargir la fenêtre EST la prolongation. Continuité au
|
|
||||||
point de suture garantie par construction (ρ(t_last) = ρ_last exactement,
|
|
||||||
constant à droite comme l'interpolation aboutit à ρ_last à gauche).
|
|
||||||
|
|
||||||
- **Formule au-delà du dernier lab** : `courbe(t) = M(t) × ρ_last` où
|
|
||||||
- `M(t)` = le modèle PK brut : porte l'« évolution classique de l'ester »
|
|
||||||
(Tmax, queues, accumulation) ET LES DOSES LOGUÉES après le dernier lab —
|
|
||||||
une nouvelle injection (override d'ester compris : EV après des labs
|
|
||||||
EEn, etc.) fait repartir la courbe en pic à l'amplitude calibrée
|
|
||||||
(épinglé par test `une dose apres le dernier lab refait monter la
|
|
||||||
courbe`) ;
|
|
||||||
- `ρ_last` = ratio lab ÷ prédiction au dernier lab significatif, supposé
|
|
||||||
CONSTANT. **Pourquoi pas une extrapolation de pente** : la pente entre
|
|
||||||
deux labs est un morphing entre deux corrections d'amplitude, pas une
|
|
||||||
tendance physiologique — l'extrapoler divergerait (un ratio qui monte
|
|
||||||
de 5 %/jour finirait en facteurs ×10 inventés). ρ_last = le meilleur
|
|
||||||
estimateur disponible, même sémantique que la calibration classique
|
|
||||||
(le facteur stocké s'applique aux doses futures).
|
|
||||||
- **Horizon (`extensionHorizonEndMs`, internal)** : dernière dose E2 +
|
|
||||||
`cutoffHours(son traitement)` — `cutoffHours` a été rendu PUBLIC (v1.6.0)
|
|
||||||
pour que la prolongation partage la MÊME sémantique d'extinction que le
|
|
||||||
moteur (10 t½ terminales V3C/WHS, fin de table ODS, 30 t½ Bateman). Au-delà,
|
|
||||||
M(t) = 0 : dessiner plus serait une ligne à zéro inventée. Retourne
|
|
||||||
`null` (pas de prolongation) si aucune dose E2 ou si le modèle est DÉJÀ
|
|
||||||
éteint au dernier lab.
|
|
||||||
- **Split visuel "LAB" / "LABX" (ChartScreen)** : `AnchoredCurve` expose
|
|
||||||
`lastAnchorMs` (timestamp du dernier ancre, `null` si < 2 ancres) ;
|
|
||||||
l'écran splitte la série au dernier ancre — "LAB" = partie ancrée (rose
|
|
||||||
plein de la v1.5.0), "LABX" = partie prolongée (MÊME couleur atténuée
|
|
||||||
α 0,55, dashed) avec SA légende (`legend_lab_track_extend` FR/EN) :
|
|
||||||
« estimation, plus ancrée ». L'estimation ne peut pas se confondre avec
|
|
||||||
le mesuré.
|
|
||||||
- **AVERTISSEMENT (demande v1.6.0)** : quand la partie prolongée est
|
|
||||||
visible, un texte s'affiche sous la légende
|
|
||||||
(`lab_track_extend_warning` FR/EN) : « simple simulation, sans garantie
|
|
||||||
de correspondre au réel — extrapole ton modèle à partir de tes résultats
|
|
||||||
de laboratoire, qui peuvent eux-mêmes être erronés ; fie-toi à ta
|
|
||||||
prochaine prise de sang ». Même condition d'affichage que la légende
|
|
||||||
LABX (`labExtendVisible`) : il ne peut jamais y avoir de courbe
|
|
||||||
prolongée sans son avertissement.
|
|
||||||
- **Leçon #63 réappliquée** : la branche `"LABX" ->` du `when` de légende
|
|
||||||
est EXPLICITE et AVANT le `else` — sinon la légende TFS serait imprimée
|
|
||||||
une 2ᵉ fois (le `else` = TFS).
|
|
||||||
- **Garde #61 inchangée** : seuls les labs significatifs ancrent — un lab
|
|
||||||
tardif hors fenêtre d'action ne prolonge PAS la courbe et n'est jamais
|
|
||||||
traversé (épinglé par test).
|
|
||||||
- **Comportement par défaut inchangé** : flag absent ou `false` =
|
|
||||||
v1.5.0 bit-compatible (testé : points identiques).
|
|
||||||
- **Limites assumées (documentées, pas des bugs)** :
|
|
||||||
- changement d'ESTER après le dernier lab : l'amplitude du nouvel ester
|
|
||||||
reste calibrée par le ρ de l'ancien — même limite que la calibration
|
|
||||||
classique (pas de lab de la nouvelle période → fallback) ;
|
|
||||||
- les doses PRÉVISIONNELLES ne participent jamais : la projection future
|
|
||||||
reste la série « Prévision » (modèles calibrés) — le tracé labs ne
|
|
||||||
consomme que des doses RÉELLES ;
|
|
||||||
- **INVARIANT v1.5.0 étendu** : la partie prolongée non plus n'entre
|
|
||||||
JAMAIS dans `levelAt` (display-end only, série "LABX" purement visuelle).
|
|
||||||
- 8 nouveaux tests dans `LabTrajectoryModelTest` (191 verts au total).
|
|
||||||
|
|
||||||
## 8. Tests unitaires
|
## 8. Tests unitaires
|
||||||
|
|
||||||
**191 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **168 sans
|
**183 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **160 sans
|
||||||
les données de test locales** (cf §8.bis : les 4 classes de régression,
|
les données de test locales** (cf §8.bis : les 4 classes de régression,
|
||||||
6/6/6/5 tests, sont skippées via `Assume`). Dépendance : JUnit 4.13.2.
|
6/6/6/5 tests, 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 =
|
||||||
@ -670,8 +589,8 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
dans le code (le scanner refuserait, et ce serait une fuite).
|
dans le code (le scanner refuserait, et ce serait une fuite).
|
||||||
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
- les tests font `Assume.assumeTrue(file.exists())` dans le `@Before` : **sans le
|
||||||
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
fichier, la classe est IGNORÉE** (skipped, pas failed) — un clone neuf ou une CI
|
||||||
exécute 168 tests au lieu de 191 (v1.6.0 ; chiffres historiques : 44/87 à v1.3.x,
|
exécute 160 tests au lieu de 183 (v1.5.0 ; chiffres historiques : 44/87 à v1.3.x,
|
||||||
150/172 à v1.4.x, 160/183 à v1.5.0) ;
|
150/172 à v1.4.x) ;
|
||||||
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
- le workdir des tests Gradle est le dossier du module (`app/`) → les tests
|
||||||
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
cherchent les fichiers à plusieurs chemins (`../local-test-data/…` en premier) ;
|
||||||
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
- **pour les lancer** : exporter un backup JSON depuis l'app → l'enregistrer sous
|
||||||
@ -797,7 +716,7 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt
|
||||||
gagne), traitements inactifs/désactivés → null.
|
gagne), traitements inactifs/désactivés → null.
|
||||||
|
|
||||||
- **`LabTrajectoryModelTest`** (19) : v1.5.0 — « Tracé labs » (§7.10) :
|
- **`LabTrajectoryModelTest`** (11) : v1.5.0 — « Tracé labs » (§7.10) :
|
||||||
passage EXACT sur chaque lab (fp 1e-6), ρ log-linéaire (fonction pure +
|
passage EXACT sur chaque lab (fp 1e-6), ρ log-linéaire (fonction pure +
|
||||||
courbe recomposée via le moteur — M recalcule et ρ = geometric mean),
|
courbe recomposée via le moteur — M recalcule et ρ = geometric mean),
|
||||||
1 lab → vide, **garde #61** (lab tardif PAS un ancrage + fenêtre bornée
|
1 lab → vide, **garde #61** (lab tardif PAS un ancrage + fenêtre bornée
|
||||||
@ -807,14 +726,6 @@ dépôt (ni maintenant, ni si le repo devient public un jour).
|
|||||||
`modelOverride` (ESE vs WHS — divergence franche : ESE≈TFS quasi
|
`modelOverride` (ESE vs WHS — divergence franche : ESE≈TFS quasi
|
||||||
identiques par CONSTRUCTION, seuil 2 % doc §8), labs T ignorés,
|
identiques par CONSTRUCTION, seuil 2 % doc §8), labs T ignorés,
|
||||||
doublons de timestamp (2ᵉ ratio gagne), bornes défensives ratioAt.
|
doublons de timestamp (2ᵉ ratio gagne), bornes défensives ratioAt.
|
||||||
v1.6.0 — **prolongation** (§7.10.bis) : identité `M × ρ_last` exacte
|
|
||||||
au-delà du dernier lab (10/20/30 j), suture exacte au dernier ancre,
|
|
||||||
horizon = cutoff du traitement (dernier point ≤ cutoff < dernier point + pas),
|
|
||||||
dose EV loguée après le dernier lab → pic de prolongation (> ×1,3 le
|
|
||||||
niveau pré-injection — la forme suit dosage + ester), flag off =
|
|
||||||
v1.5.0 bit-compatible, garde #61 sur la prolongation (le lab tardif ne
|
|
||||||
prolonge ni n'ancre), gardes du helper d'horizon (pas de dose / modèle
|
|
||||||
éteint → null), `lastAnchorMs` (null si < 2 ancres).
|
|
||||||
|
|
||||||
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
**Ce que les tests ont déjà attrapé** : bisection inversée de `computeKa` (présente depuis
|
||||||
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
la session 1 !), plancher 0,01 des queues de profils, mapping silencieux du modèle inconnu.
|
||||||
@ -1001,13 +912,6 @@ triangles ▲▼ via `PharmacokineticEngine.detectExtrema` **accompagnés de la
|
|||||||
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
|
valeur estimée** du pic (au-dessus) ou du creux (en dessous), dans la couleur
|
||||||
de la série (`drawExtremum`).
|
de la série (`drawExtremum`).
|
||||||
|
|
||||||
**Séries « Tracé labs » (v1.5.0/v1.6.0)** : le ChartScreen peut ajouter les
|
|
||||||
clés "LAB" (partie ANCRÉE — passe par les labs, rose #C2185B dashed) et
|
|
||||||
"LABX" (partie PROLONGÉE — M × ρ_last constant, même rose atténué α 0,55)
|
|
||||||
— cf §7.10 / §7.10.bis pour le moteur et les gardes ; les deux clés sont
|
|
||||||
SKIPPÉES par la boucle de légende (leçon #63) et ont leurs entrées dédiées
|
|
||||||
+ avertissement « simulation » quand LABX est visible.
|
|
||||||
|
|
||||||
**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée
|
**Zoom (v1.2.9)** : pinch **et** boutons − / + — plage temporelle bornée
|
||||||
**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu
|
**6 h → 300 j** (`MIN_RANGE_H`/`MAX_RANGE_H`), point focal du pinch maintenu
|
||||||
fixe dans le temps (formule centroid), échantillonnage **adaptatif**
|
fixe dans le temps (formule centroid), échantillonnage **adaptatif**
|
||||||
@ -1603,8 +1507,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` — 191 au
|
2. **Tests verts obligatoires** : `./gradlew testDebugUnitTest` — 183 au
|
||||||
total, 168 si `local-test-data/` est absent (les 4 classes de régression
|
total, 160 si `local-test-data/` est absent (les 4 classes de régression
|
||||||
réelles sont skippées via `Assume`, 6/6/6/5 tests) ; **lint vert
|
réelles sont skippées via `Assume`, 6/6/6/5 tests) ; **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 :
|
||||||
@ -1932,14 +1836,6 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
du graphique (toggles modèles, calibration) n'est pas modifié ; les
|
du graphique (toggles modèles, calibration) n'est pas modifié ; les
|
||||||
jours sans labs récents : la courbe s'arrête au dernier lab (le
|
jours sans labs récents : la courbe s'arrête au dernier lab (le
|
||||||
vieillir est normal) ; chip décoché = plus de série ; 0 crash
|
vieillir est normal) ; chip décoché = plus de série ; 0 crash
|
||||||
- [ ] **v1.6.0** : **Prolonger le tracé labs** : avec `Tracé labs` ON,
|
|
||||||
activer `Prolonger` → la courbe continue APRÈS le dernier lab
|
|
||||||
(rose atténué) jusqu'à l'extinction du modèle, avec légende dédiée
|
|
||||||
ET AVERTISSEMENT « simple simulation, sans garantie… » sous la
|
|
||||||
légende ; une dose loguée après le dernier lab fait repartir la
|
|
||||||
courbe en pic ; désactiver `Prolonger` → retour à la fenêtre
|
|
||||||
[1er ; dernier lab] exacte ; `Tracé labs` OFF → le chip `Prolonger`
|
|
||||||
est désactivé ; l'avertissement disparaît avec la série
|
|
||||||
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
- [ ] **v1.4.10** : **pan sur la vue 24 h** : glisser horizontalement à
|
||||||
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
24 h doit déplacer la courbe (fix #62 : les deltas < 1 h s'accumulent
|
||||||
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
— avant, le pan ne bougeait jamais sur 24 h) ; tester aussi à 7 j et
|
||||||
@ -1960,7 +1856,7 @@ Sur le téléphone de test (à compléter par l'utilisatrice) :
|
|||||||
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 12 sept. 2026 (v1.6.0) — build OK, lint vert, 191/191 tests verts (168 sans
|
*Doc mise à jour le 11 sept. 2026 (v1.5.0) — build OK, lint vert, 183/183 tests verts (160 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
|
||||||
|
|||||||
@ -204,20 +204,6 @@ Pics de référence (dose unique de 5 mg, IM) :
|
|||||||
- **Pics / creux** (chip sur le graphique) : triangles ▲▼ aux extrema estimés de
|
- **Pics / creux** (chip sur le graphique) : triangles ▲▼ aux extrema estimés de
|
||||||
chaque courbe (E2 et T, les deux modèles) — pratique pour visualiser d'un coup
|
chaque courbe (E2 et T, les deux modèles) — pratique pour visualiser d'un coup
|
||||||
d'œil les hauts et les bas entre deux injections
|
d'œil les hauts et les bas entre deux injections
|
||||||
- **Tracé labs** (chip `Tracé labs`, off par défaut) : une courbe E2 qui suit la
|
|
||||||
forme du modèle PK MAIS passe EXACTEMENT par chacune de tes prises de sang —
|
|
||||||
la trajectoire que tes labs tracent, avec la dynamique du modèle entre deux
|
|
||||||
analyses (rose pointillé). Entre le 1er et le dernier lab seulement : la
|
|
||||||
courbe ne s'invente pas de niveau.
|
|
||||||
**Prolonger** (chip, off par défaut — actif seulement quand Tracé labs est on) :
|
|
||||||
la courbe se PROLONGE après ton dernier lab : le modèle (dosages, esters
|
|
||||||
injectés depuis, pics et descentes classiques) × le ratio mesuré à ton dernier
|
|
||||||
lab. La partie prolongée est dessinée en rose ATTÉNUÉ avec sa légende
|
|
||||||
(« estimation, plus ancrée ») et un AVERTISSEMENT : c'est une simple
|
|
||||||
simulation sans garantie de correspondre au réel, extrapolée à partir de tes
|
|
||||||
résultats de laboratoire — qui peuvent eux-mêmes être erronés. Elle s'arrête
|
|
||||||
quand le modèle lui-même s'éteint. Compare-la à ta prochaine prise de sang,
|
|
||||||
et fie-toi à elle plutôt qu'à cette courbe
|
|
||||||
- **Calibration automatique** (Paramètres, désactivée par défaut) : quand activée, les
|
- **Calibration automatique** (Paramètres, désactivée par défaut) : quand activée, les
|
||||||
facteurs d'échelle et le modèle T sont ajustés en continu depuis tes labs — pour
|
facteurs d'échelle et le modèle T sont ajustés en continu depuis tes labs — pour
|
||||||
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user