diff --git a/README.md b/README.md index 93e0923..78b6dc2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > sang et aux consignes de ton endocrinologue. -- **Statut** : v1.8.2 — build Android ✅, **lint vert** ✅, **223 tests unitaires** ✅ (193 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ +- **Statut** : v1.9.0 — build Android ✅, **lint vert** ✅, **234 tests unitaires** ✅ (204 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **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) @@ -48,6 +48,13 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** extinction du modèle, partie estimée dessinée atténuée + légende dédiée) — 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 Estrannaise ANALYTIQUE (v1.9.0)** : le modèle ESE utilise désormais + la forme close 3C publiée par estrannaise.js (fidélité aux anciennes tables + épinglée RMS 0) — **nuage d'incertitude MCMC** (chip `Nuage`, exclusif ESE : + 32 courbes du posterior montrant la plage d'imprécision, comme sur le site + estrannaise) ; **6 esters injectables** (dont EUCS, exclusif) ; l'ODS + historique n'est plus utilisé au runtime (fidélité = tests uniquement) ; + t½ terminale analytique (recommandation de prise de sang) - **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 @@ -116,7 +123,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1. git clone && cd HormoneTrack echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME ./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk -./gradlew testDebugUnitTest # 223 tests (193 sans les données locales) +./gradlew testDebugUnitTest # 234 tests (204 sans les données locales) ./gradlew lint # lint vert obligatoire avant release ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 542662e..5855da4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { // tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0" // (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans // les APK publiés (Paramètres + titre du dialog « Nouveautés »). - versionCode = 37 - versionName = "1.8.2" + versionCode = 38 + versionName = "1.9.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/assets/mcmc_samples.json b/app/src/main/assets/mcmc_samples.json new file mode 100644 index 0000000..64e5d3f --- /dev/null +++ b/app/src/main/assets/mcmc_samples.json @@ -0,0 +1 @@ +{"EV":[[881.2,0.177,2.58,1.91],[862.7,0.198,2.86,2.01],[514.4,0.208,11.92,1.19],[509.9,0.218,6.16,1.2],[639.5,0.224,3.03,1.6],[304.6,0.308,10.33,0.86],[426,0.245,6.34,1.1],[575.4,0.234,3.22,1.56],[626,0.205,12.96,1.45],[651.8,0.212,3.49,1.63],[788.5,0.2,2.64,1.85],[542.6,0.208,5.2,1.4],[529,0.229,4.98,1.32],[521.8,0.233,4.88,1.39],[235.2,0.364,10.7,0.67],[341.2,0.271,13.95,0.93],[521,0.199,8.33,1.23],[627.5,0.219,4.05,1.44],[570.3,0.224,4.52,1.46],[339.9,0.282,7.38,0.98],[670.4,0.228,3.06,1.65],[517.3,0.233,3.46,1.42],[374.5,0.246,14.46,1],[761.1,0.222,2.71,1.91],[697.2,0.218,2.92,1.68],[641.6,0.214,2.95,1.6],[466.2,0.228,9.95,1.17],[675.2,0.245,1.98,1.81],[473.3,0.238,6.12,1.21],[454.1,0.225,7.47,1.21],[743.2,0.207,3.26,1.84],[663.2,0.237,2.28,1.75],[509.1,0.223,4.49,1.33],[451.4,0.23,42.12,1.18],[579.1,0.23,2.6,1.56],[547,0.241,3.2,1.44],[757.5,0.201,2.55,1.8],[459.8,0.272,4.13,1.26],[852.5,0.213,2.23,2.17],[398.4,0.272,5.91,1.11],[415.2,0.234,18.89,1.03],[941.7,0.191,3.32,2.34],[564.8,0.252,2.52,1.6],[543.5,0.215,4.97,1.29],[475.8,0.218,8.92,1.19],[835,0.2,2.29,2.04],[575.4,0.232,5.34,1.39],[423.8,0.246,10.75,1.08],[373.6,0.272,8.51,1.04],[608.6,0.189,6.01,1.42],[515.5,0.25,4.81,1.41],[459.3,0.244,4.57,1.24],[341.7,0.269,4.73,0.98],[687,0.193,3.67,1.68],[854.9,0.208,2.23,2.09],[928.8,0.202,2.5,2.21],[454.2,0.21,7.11,1.13],[429.5,0.255,11.53,1.17],[434.2,0.216,7.07,1.12],[830,0.188,2.65,1.95],[740.5,0.189,2.92,1.65],[580.3,0.234,2.98,1.54],[325.4,0.295,7.09,0.88],[531.6,0.241,3.23,1.45],[391.9,0.231,10.47,0.99],[750.1,0.218,2.67,1.81],[795,0.186,4.22,1.9],[572.6,0.252,4.05,1.57],[644.7,0.252,2.26,1.75],[363,0.263,8.56,0.98],[407.5,0.241,7.98,1.08],[616.8,0.214,4.48,1.49],[440.6,0.244,9.07,1.2],[335.9,0.285,18.49,0.96],[623,0.209,5.29,1.53],[359.9,0.255,23.75,0.91],[565.1,0.205,5.45,1.38],[445.8,0.22,7.03,1.16],[385.1,0.261,10.51,1.01],[370.9,0.32,6.56,1.11],[591,0.219,3.89,1.46],[327.4,0.277,7.81,0.95],[449.3,0.246,6.04,1.22],[594.3,0.236,2.77,1.53],[391,0.259,16.26,1.11],[542,0.211,4.41,1.38],[566.4,0.204,5.42,1.33],[608.8,0.237,2.65,1.62],[805,0.201,3.71,1.83],[286.1,0.325,21.79,0.8],[484.2,0.24,5.16,1.27],[690.9,0.222,3.32,1.75],[781.2,0.219,2.12,1.91],[965.2,0.211,3.05,2.4],[401.8,0.237,8.77,1.01],[491.4,0.228,8.16,1.25],[900.7,0.205,2.29,2.11],[618.9,0.207,2.76,1.39],[428.9,0.23,21.47,1.09],[392.1,0.234,28.31,1.01],[756.5,0.213,2.61,1.88],[469.6,0.23,4,1.28],[359.2,0.267,15.39,0.99],[587.5,0.232,2.2,1.59],[517.9,0.247,3.55,1.39],[568,0.237,3.65,1.48],[445.2,0.233,20.38,1.15],[670,0.225,2.46,1.61],[550.8,0.211,4.67,1.31],[555.4,0.24,3.82,1.54],[657.4,0.217,3.78,1.68],[401.5,0.258,7.13,1.07],[536.3,0.233,3.98,1.32],[383.7,0.275,6.06,1.04],[455,0.236,5.19,1.27],[563.5,0.214,8.39,1.34],[484.2,0.239,5.44,1.3],[679,0.233,2.79,1.76],[380.7,0.275,6.86,1.1],[399.8,0.272,4.63,1.09],[457.7,0.239,5.01,1.16],[490.6,0.222,7.48,1.27],[784.9,0.219,2.43,1.95],[295.1,0.309,7.66,0.88],[965.3,0.2,2.51,2.31],[438.4,0.233,8.3,1.19],[477.6,0.208,16.31,1.12],[517.4,0.229,3.76,1.26],[749.3,0.2,2.63,1.79],[695.4,0.183,4.33,1.47],[366.4,0.248,15.67,1],[437.5,0.25,5.3,1.18],[475.3,0.228,6.81,1.26],[701.8,0.182,4.22,1.58],[435.3,0.244,5.12,1.15],[407.4,0.247,10.69,1.07],[353,0.243,20.43,0.92],[362.3,0.271,8.58,0.97],[577.8,0.211,3.71,1.4],[379.7,0.262,13.79,0.96],[483.6,0.247,3.79,1.3],[659,0.24,4.01,1.75],[565.2,0.239,3.12,1.55],[628,0.189,5.19,1.49],[639.5,0.202,3.32,1.53],[498.4,0.234,3.94,1.34],[401.9,0.26,49.43,1.05],[374.6,0.272,7.93,1.01],[580.4,0.281,2.5,1.54],[488.9,0.224,10.88,1.23],[326.7,0.262,48.29,0.87],[414.4,0.275,7.9,1.11],[674.4,0.215,3.37,1.64],[556.3,0.199,9.88,1.33],[844.5,0.189,4.27,2.01],[415.1,0.26,6.76,1.11],[455.9,0.227,8.84,1.07],[523.1,0.229,4.55,1.33],[600.9,0.204,5.31,1.36],[480.7,0.234,12.68,1.24],[317.2,0.312,7.11,0.91],[376.5,0.271,25.19,1.05],[801.8,0.179,4.17,1.71],[427.7,0.223,4.74,1.13],[685,0.201,3.43,1.69],[292.2,0.306,21.63,0.85],[550.4,0.205,4,1.39],[464.6,0.219,12.46,1.13],[281.5,0.314,28.14,0.81],[349.2,0.264,11.5,0.96],[308.2,0.28,14.43,0.85],[341.4,0.25,19.02,0.87],[742.2,0.252,2.66,1.99],[509.1,0.226,5.87,1.35],[419.2,0.225,33.37,1.07],[579,0.24,2.77,1.49],[462.4,0.233,6.57,1.18],[502.2,0.218,4.98,1.23],[317.8,0.311,12.21,0.96],[456.5,0.217,9.1,1.08],[420.9,0.279,6.48,1.15],[401.2,0.271,7.67,1.18],[505.3,0.233,5.4,1.28],[506.5,0.273,3.03,1.38],[328.3,0.261,11.74,0.88],[392.6,0.252,12.23,1.05],[689.8,0.234,2.62,1.85],[648.4,0.224,3.3,1.57],[426.4,0.233,13.69,1.12],[423.6,0.228,8.71,1.09],[660.9,0.221,2.94,1.7],[542.9,0.239,3.33,1.49],[496,0.237,5.12,1.3],[642.1,0.189,5.05,1.5],[642.1,0.206,3.38,1.6],[476.6,0.231,6.76,1.23],[345.6,0.272,17.87,0.93],[390.7,0.252,14.64,1.04],[226.7,0.375,27.43,0.66],[478.9,0.239,4.26,1.24],[828.4,0.216,2.22,2.03],[799.4,0.207,2.69,1.99],[654.3,0.217,2.68,1.61],[831.9,0.229,2.4,2.09],[289.5,0.3,11.23,0.8],[580.4,0.232,4.03,1.43],[723.8,0.204,4.55,1.69],[430.6,0.277,3.27,1.18],[490.6,0.221,12.38,1.18],[485.6,0.203,12.11,1.17],[586.4,0.228,3.83,1.42],[707,0.223,2.89,1.77],[411,0.273,4.13,1.18],[410.1,0.29,3.56,1.17],[493.1,0.222,6.57,1.24],[622.3,0.227,2.83,1.56],[892.3,0.221,2.26,2.21],[451.3,0.22,17.39,1.06],[642.4,0.225,3.06,1.64],[599.1,0.212,4.3,1.5],[302.2,0.296,5.57,0.9],[451.9,0.251,8.91,1.25],[377.6,0.286,7.72,1.11],[420.9,0.265,5.39,1.18],[487.6,0.224,4.39,1.18],[813.5,0.204,3.36,1.94],[405.3,0.256,11.01,1.1],[487.8,0.275,3.57,1.35],[441.2,0.249,5.29,1.18],[442,0.205,18.31,1.03],[693,0.207,3.14,1.63],[426.9,0.246,10.63,1.17],[487,0.24,2.85,1.21],[478,0.236,4.85,1.24],[478.3,0.187,26.66,1.16],[412.8,0.237,26.25,1.05],[310.8,0.296,10,0.9],[866.9,0.191,2.59,1.94],[649.5,0.189,5.52,1.5],[422.3,0.24,23.08,1.04],[457.7,0.225,19.29,1.13],[341.5,0.253,9.07,0.9],[720.4,0.215,2.94,1.9],[761.8,0.244,2.25,2.02],[500.5,0.216,6.91,1.22],[493.7,0.225,7.5,1.21],[575.9,0.211,4.53,1.39],[599.9,0.233,4.29,1.52],[534.7,0.231,4.36,1.4],[461.5,0.24,12.33,1.18],[501,0.223,5.96,1.3],[562.4,0.228,3.2,1.43],[895.3,0.215,2.57,2.24],[471.3,0.219,7.64,1.17],[648.2,0.206,3.14,1.57],[741.6,0.211,2.68,1.88],[630.7,0.185,7.04,1.41],[472.9,0.256,5.01,1.3],[434.2,0.261,26.85,1.08],[384.2,0.256,46.26,0.99],[371.4,0.258,15.05,1],[330.9,0.291,9.99,0.96],[567.2,0.213,3.65,1.43],[611.1,0.279,2.64,1.66],[609.4,0.238,2.87,1.63],[564.8,0.244,3.36,1.49],[519.1,0.215,6.36,1.31],[352,0.275,7.87,0.94],[475.9,0.209,16.84,1.15],[718.2,0.179,3.85,1.54],[394.9,0.239,12.58,0.98],[505.3,0.253,3.08,1.31],[341.8,0.279,28.09,0.97],[521.2,0.225,4.62,1.33],[798.3,0.173,4.51,1.76],[806.1,0.207,2.5,1.87],[437.2,0.225,18.26,1.07],[963.8,0.203,2.42,2.39],[349.5,0.259,21.06,0.93],[411.3,0.252,22.71,1.08],[387.4,0.263,9.38,1.05],[547,0.247,2.96,1.51],[632.7,0.202,4.67,1.51],[643.1,0.202,3.52,1.58],[795.8,0.214,2.98,2.03],[463.3,0.217,13.61,1.13],[746.9,0.215,2.46,1.91],[484.9,0.26,4.61,1.36],[897,0.21,2.31,2.25],[975.5,0.193,2.32,2.29],[494.9,0.233,3.43,1.22],[525.2,0.221,5.25,1.33],[299.3,0.311,11.99,0.91],[667.4,0.201,4.36,1.78],[446.1,0.236,4.8,1.22],[447.8,0.22,8.54,1.06],[449.9,0.25,3.32,1.27],[391.2,0.254,8.63,1.06],[630.9,0.208,3.18,1.47],[500.4,0.237,4.18,1.35],[718.7,0.213,3.35,1.8],[836.2,0.187,3.5,1.98],[423.1,0.238,14.33,1.05],[549.2,0.193,11.16,1.27],[402.6,0.236,22.9,1.04],[340.1,0.292,9.25,0.95],[833.2,0.184,5.34,1.87],[704.5,0.185,7.23,1.62],[575.6,0.235,3.09,1.55],[585.8,0.214,5,1.47],[260.9,0.345,7.31,0.79],[509.4,0.214,5.95,1.27],[497.8,0.223,5.54,1.21]],"EU":[[213.28,0.03122,7.942,1.565],[183.07,0.02819,20.323,1.334],[198.44,0.04313,32.624,1.963],[239.24,0.03109,11.828,1.89],[59.41,0.03921,9.922,0.537],[2486.41,0.0048,9.173,3.567],[507.35,0.01423,10.32,2.02],[210.69,0.02583,15.3,1.452],[326.74,0.03264,22.283,2.619],[465.99,0.01924,4.606,2.391],[704.2,0.0138,6.049,2.587],[196.21,0.03774,4.523,1.723],[259.14,0.0222,45.771,1.635],[161.44,0.0347,5.315,1.293],[718.52,0.01676,4.228,3.432],[483,0.0148,7.717,1.948],[661.97,0.00992,26.896,2.044],[98.8,0.03951,39.241,0.979],[678.68,0.01605,13.888,2.989],[580.87,0.01241,11.598,2.106],[849.1,0.01159,15.434,2.916],[199.94,0.03085,7.339,1.323],[243.06,0.02872,9.709,1.793],[186.12,0.02966,5.059,1.423],[179.38,0.03302,18.768,1.599],[214.15,0.02717,17.043,1.52],[1361.01,0.00696,6.105,2.771],[265.23,0.02159,3.832,1.541],[682.12,0.01276,8.6,2.497],[1211.11,0.00748,4.543,2.565],[991.91,0.0107,4.186,3.421],[1355.26,0.00652,7.197,2.339],[261.57,0.01914,17.939,1.395],[546.77,0.02596,14.07,3.637],[270.77,0.02597,8.034,1.929],[177.25,0.0257,11.989,1.143],[216.66,0.04504,4.81,2.357],[915.35,0.0136,11.76,3.494],[176.64,0.03235,3.495,1.365],[4061.69,0.00186,7.833,2.273],[190.88,0.02593,3.986,1.529],[149.93,0.03889,40.951,1.297],[288.72,0.0342,10.156,2.311],[211.58,0.02081,13.848,1.198],[657.59,0.01011,25.904,1.928],[162.85,0.03234,11.284,1.179],[120.16,0.04265,19.133,1.217],[6478.64,0.00148,6.72,2.785],[62.34,0.06761,13.917,1.015],[346.99,0.02651,3.648,2.655],[272.92,0.01771,11.589,1.372],[567.8,0.01195,16.813,2.075],[441.28,0.02962,10.936,3.51],[301.3,0.02717,8.828,2.006],[165.46,0.02553,9.457,1.195],[409.53,0.00994,18.444,1.079],[350.46,0.02536,6.2,2.293],[699.64,0.00912,4.052,1.903],[518.02,0.01929,3.072,2.618],[208.7,0.02555,17.051,1.368],[367.39,0.01294,9.138,1.26],[239.02,0.02357,10.935,1.451],[202.58,0.03734,6.506,1.866],[627.34,0.01948,8.965,3.357],[176.35,0.03326,10.733,1.596],[224.71,0.04028,25.292,2.088],[119.78,0.03652,21.754,1.046],[588.77,0.01775,25.268,2.926],[295.78,0.02811,47.071,2.143],[413.47,0.02239,3.458,2.554],[101.3,0.05038,19.29,1.462],[290.95,0.03265,8.571,2.221],[202.12,0.03565,4.84,1.621],[618.37,0.01954,4.685,3.287],[307.39,0.01304,16.094,1.071],[456.89,0.01446,4.926,1.72],[159.51,0.02004,12.634,0.788],[304.28,0.03745,2.96,2.681],[995.06,0.00524,8.256,1.508],[515.74,0.01586,7.305,2.086],[476.87,0.02031,4.862,2.673],[292.34,0.03545,15.102,2.859],[263.13,0.0186,19.609,1.244],[256.28,0.02065,7.917,1.409],[241.98,0.02156,4.658,1.396],[191.93,0.0314,3.438,1.57],[548.11,0.01802,3.365,2.722],[264.41,0.0372,5.302,2.31],[550.04,0.01319,7.126,1.979],[883.35,0.01085,8.852,2.769],[543.18,0.02491,49.754,3.58],[226.95,0.02796,20.775,1.697],[67.69,0.05193,13.668,0.7],[542.13,0.01734,4.476,2.437],[514.25,0.01251,13.457,1.758],[365.34,0.02255,9.424,2.16],[267.17,0.02693,8.26,1.803],[386.08,0.02469,6.485,2.555],[458.41,0.01144,4.408,1.278],[614.85,0.0196,3.947,3.224],[68.29,0.06043,18.96,0.822],[239.23,0.0089,26.063,0.596],[4733.55,0.00157,5.496,1.973],[443.55,0.01741,6.631,1.877],[765.96,0.01502,10.226,3.713],[328.29,0.01158,7.476,1.023],[568.12,0.0167,8.388,2.727],[1325.91,0.00806,5.801,2.929],[2581.3,0.00405,13.097,3.152],[349.88,0.01826,18.646,1.737],[212.6,0.02596,25.541,1.535],[599.26,0.01757,17.738,2.524],[237.96,0.03624,13.648,1.86],[471.48,0.01336,15.038,1.809],[732.87,0.01174,12.18,2.52],[490.08,0.01518,4.281,1.892],[126.45,0.03226,4.018,0.975],[700.35,0.02739,11.971,4.354],[2453.31,0.00377,12.303,2.686],[74.92,0.06187,23.62,0.905],[68.35,0.08335,3.076,1.135],[229.13,0.03455,63.501,1.801],[2991.44,0.0045,18.094,4.084],[160.52,0.02704,11.734,1.097],[140.83,0.03365,12.017,1.161],[622.54,0.01886,9.003,3.253],[117.66,0.02492,6.31,0.767],[300.54,0.02566,28.151,2.016],[318.02,0.03635,4.026,2.663],[344.58,0.03326,3.329,2.383],[254.96,0.03306,8.868,1.991],[78.8,0.05793,9.758,0.948],[512.65,0.02007,26.056,2.974],[94.78,0.04582,10.046,1.093],[344.22,0.01579,39.528,1.562],[754.54,0.00957,19.035,2.001],[857.03,0.01282,10.543,3.411],[900.72,0.00998,16.62,2.557],[603.72,0.01546,44.974,2.69],[206.88,0.02709,8.227,1.591],[112.26,0.03868,10.981,0.978],[579.68,0.01307,16.766,2.031],[382.63,0.01693,6.668,1.98],[755.73,0.01109,6.707,2.417],[338.38,0.02875,11.58,2.272],[216.61,0.03084,21.161,1.699],[725.64,0.01934,4.089,3.893],[100.33,0.03873,5.103,0.959],[758.17,0.01704,11.177,3.709],[120.79,0.03065,6.075,0.718],[417.95,0.00901,22.95,1.035],[154.02,0.03643,4.767,1.314],[332.64,0.03288,16.974,2.636],[146.74,0.03623,7.301,1.338],[643.87,0.01466,11.831,2.75],[624.68,0.01835,9.887,3.093],[361.05,0.02737,2.838,2.785],[16693.74,0.00055,15.309,2.964],[31.11,0.08967,16.897,0.423],[176.32,0.02616,17.15,1.347],[94.95,0.06209,14.02,1.163],[164.19,0.01715,49.023,0.712],[342.39,0.03119,27.612,2.917],[164.82,0.02527,29.811,1.025],[164.9,0.03521,19.532,1.306],[640.43,0.00854,6.34,1.592],[615.94,0.01913,16.562,3.152],[135.08,0.02931,4.136,1.131],[83.16,0.06685,1.477,1.158],[1126.09,0.01178,18.859,4.002],[656.62,0.01026,8.078,1.784],[16.6,0.11474,9.925,0.224],[115.87,0.02529,11.255,0.757],[148.77,0.02757,2.873,1.072],[326.27,0.02589,5.739,2.115],[274.1,0.02468,7.976,1.544],[313.9,0.01503,18.553,1.395],[231.36,0.03595,10.697,2.189],[515.74,0.01147,5.818,1.947],[277.57,0.02337,5.996,1.523],[179.66,0.03421,30.626,1.503],[637.64,0.0087,8.493,1.579],[647.04,0.0074,76.477,1.538],[929.21,0.00497,20.162,1.552],[1796.36,0.00359,14.782,1.895],[2169.23,0.0048,7.716,3.1],[243.51,0.03074,7.542,1.887],[429.14,0.02069,6.932,2.439],[240.49,0.03255,6.026,1.917],[322.13,0.02129,12.352,1.846],[416.57,0.02171,8.893,2.421],[241.82,0.02378,7.444,1.557],[446,0.01754,7.246,2.218],[218.45,0.0268,12.988,1.5],[883.53,0.01279,15.304,3.102],[647.27,0.01126,20.552,2.237],[457.98,0.01746,56.574,2.147],[260.33,0.03407,23.712,2.329],[140.32,0.03429,17.606,1.315],[352.68,0.03078,13.999,2.728],[213.08,0.02664,8.01,1.332],[490.99,0.0146,11.952,2.01],[116.12,0.04888,17.808,1.3],[136.74,0.04601,14.658,1.411],[194.17,0.04154,2.584,1.849],[283.48,0.01856,6.612,1.458],[1016.02,0.01032,18.879,3.128],[145.32,0.0375,5.916,1.491],[589.18,0.01212,6.234,2.321],[487.16,0.01063,5.996,1.584],[504.57,0.02613,9.804,3.398],[218.82,0.03762,8.214,2.043],[345.65,0.02806,33.24,2.646],[495.6,0.02369,3.813,2.695],[665.06,0.01295,9.778,2.567],[41.19,0.06215,11.858,0.515],[490.41,0.02078,30.35,2.704],[276.06,0.03083,42.893,1.933],[363.14,0.04134,4.123,3.857],[147.43,0.02692,9.48,1.024],[206.41,0.02619,6.876,1.338],[202.88,0.02707,9.081,1.462],[712.22,0.01193,6.782,2.414],[845.47,0.00546,14.132,1.338],[189.94,0.04425,25.058,1.869],[321.56,0.0276,36.743,2.283],[53.06,0.05303,16.932,0.649],[63.91,0.04651,10.447,0.605],[317.13,0.0145,8.612,1.296],[297.58,0.02267,9.805,1.691],[481.91,0.01934,21.526,2.342],[493.65,0.01292,6.265,1.764],[144.42,0.02171,10.114,0.88],[150.37,0.0211,9.016,0.777],[60.37,0.03682,9.153,0.45],[355.57,0.02307,7.321,2.004],[2087.53,0.00412,7.329,2.648],[234.28,0.0258,17.372,1.668],[167.65,0.0402,6.717,1.67],[29.54,0.09246,1.814,0.375],[22.58,0.093,37.665,0.314],[593.78,0.0117,23.549,1.874],[96.39,0.03208,23.675,0.732],[60.71,0.05381,3.091,0.754],[173.88,0.0328,19.352,1.529],[176.64,0.03632,14.195,1.45],[224.74,0.0291,3.125,1.56],[703.85,0.01378,8.666,2.709],[382.52,0.02616,6.658,2.468],[468.82,0.01988,7.375,2.398],[448.92,0.02808,6.557,3.171],[860.66,0.01072,5.299,2.606],[412.78,0.02513,13.818,2.721],[381.98,0.01621,8.74,1.639],[136.36,0.03377,6.771,1.195],[298.22,0.03547,9.94,2.518],[517.55,0.01531,5.409,2.082],[291.1,0.02223,4.053,1.852],[471.06,0.01139,32.452,1.466],[194.59,0.05029,5.102,2.534],[1790.68,0.01036,6.973,5.386],[199.36,0.02459,7.187,1.231],[358.93,0.0172,2.479,1.92],[2336.02,0.00612,11.047,4.079],[140.88,0.0577,8.486,1.771],[249.86,0.01711,2.935,1.151],[1049.25,0.00536,11.259,1.673],[467.05,0.01322,58.104,1.958],[295.3,0.03094,18.18,2.572],[217.3,0.02609,16.368,1.622],[177.54,0.01907,20.631,0.856],[322.97,0.02607,8.418,2.102],[198.07,0.02236,1.971,1.166],[1597.63,0.00366,2.256,1.476],[1898.06,0.00516,8.496,2.732],[242.65,0.02566,3.834,1.717],[1195.85,0.00842,16.001,2.954],[265.1,0.01334,8.944,0.962],[1513.46,0.00593,2.69,2.56],[99.87,0.03372,10.032,0.876],[319.43,0.02301,18.207,1.93],[487.24,0.00956,32.358,1.155],[260.59,0.03367,13.043,1.993],[11.08,0.12553,48.891,0.185],[1082.74,0.00816,4.74,2.543],[611.64,0.00727,8.651,1.282],[307.94,0.01762,6.506,1.652],[677.41,0.01108,12.811,2.295],[76.34,0.04303,4.354,0.735],[359.49,0.01674,7.475,1.599],[712.03,0.0122,5.177,2.529],[314.71,0.02557,28.776,2.012],[268.1,0.00889,15.632,0.78],[286.36,0.02474,4.436,1.757],[117.41,0.03797,32.435,1.02],[275.28,0.03297,5.773,2.173],[186.74,0.02584,1.861,1.268],[879.12,0.00715,5.656,1.956],[472.88,0.0132,8.506,1.673],[134.17,0.05625,3.129,1.781],[485.38,0.01307,8.669,1.664],[478.31,0.02348,6.72,2.689],[645.83,0.01418,5.188,2.343],[175.19,0.03678,15.366,1.742],[162.8,0.03509,5.875,1.318],[836.39,0.00465,6.672,1.067],[895.67,0.01953,17.841,4.522],[131.73,0.03457,15.922,1.137],[498.75,0.02615,7.787,3.441],[298.42,0.01803,6.433,1.589],[328.58,0.01961,11.472,1.658],[349.19,0.01651,4.472,1.631],[204.68,0.02531,13.752,1.299]],"EEn":[[107.7,0.163,0.779,0.249],[255.2,0.111,0.535,0.514],[154,0.119,1.029,0.309],[177.2,0.123,0.619,0.381],[220.9,0.127,0.587,0.501],[140,0.13,1.161,0.29],[100.8,0.149,2.718,0.199],[92.4,0.161,3.972,0.184],[128.2,0.145,0.743,0.295],[217.2,0.112,0.678,0.451],[86,0.203,0.952,0.206],[113.5,0.171,0.768,0.275],[151.3,0.141,0.571,0.348],[101.6,0.152,1.862,0.212],[98.4,0.159,0.825,0.211],[139.9,0.129,1.202,0.302],[128.6,0.142,0.732,0.286],[97.1,0.152,2.965,0.187],[168.1,0.13,0.662,0.361],[89.5,0.172,1.409,0.194],[201.2,0.114,0.688,0.424],[85.6,0.188,1.163,0.2],[86.4,0.158,1.622,0.169],[85.7,0.183,0.98,0.186],[117.3,0.135,1.176,0.233],[196.5,0.118,0.676,0.405],[159.5,0.13,0.845,0.358],[96.8,0.168,1.12,0.215],[177.2,0.114,0.792,0.363],[89.7,0.184,0.899,0.206],[118.7,0.149,0.892,0.272],[108.9,0.151,1.375,0.229],[194.9,0.118,0.655,0.411],[145.2,0.125,1.012,0.304],[99.6,0.151,1.575,0.201],[237.5,0.111,0.534,0.481],[92.5,0.159,1.618,0.191],[115.4,0.135,1.826,0.231],[108.4,0.134,11.625,0.207],[124.8,0.134,1.125,0.243],[252.3,0.094,0.652,0.552],[96.8,0.161,15.378,0.202],[166.3,0.122,1.003,0.348],[102.8,0.161,1.406,0.228],[99.9,0.149,3.795,0.202],[158.5,0.138,0.675,0.364],[133.3,0.123,1.541,0.268],[231.7,0.116,0.593,0.483],[131.9,0.126,1.205,0.26],[207.9,0.113,0.676,0.426],[108.7,0.151,1.152,0.237],[87.4,0.165,3.245,0.173],[106.3,0.145,1.935,0.216],[136.9,0.126,0.808,0.285],[189.6,0.132,0.505,0.434],[142.3,0.12,1.362,0.282],[100.1,0.165,1.673,0.223],[113.2,0.139,1.058,0.24],[85,0.16,3.606,0.164],[226.8,0.115,0.548,0.476],[194.6,0.117,0.779,0.412],[195.6,0.111,0.798,0.396],[90.3,0.169,1.945,0.186],[153.6,0.119,0.993,0.305],[248.3,0.115,0.561,0.526],[206.7,0.115,0.639,0.418],[136,0.129,1.288,0.284],[105.3,0.153,1.03,0.229],[205.5,0.12,0.571,0.423],[131.4,0.131,1.179,0.276],[118.3,0.162,1.293,0.28],[98.1,0.159,1.175,0.205],[150,0.127,1.084,0.316],[261,0.121,0.585,0.571],[193.6,0.126,0.643,0.432],[208.1,0.116,0.696,0.441],[98.8,0.163,1.66,0.217],[163.8,0.124,0.779,0.351],[140.7,0.133,0.847,0.294],[93.4,0.157,1.899,0.178],[180.6,0.132,0.645,0.408],[139.4,0.135,0.801,0.306],[141.5,0.128,0.805,0.306],[88.2,0.163,2.701,0.174],[88.4,0.178,1.589,0.191],[152,0.122,1.029,0.306],[86.6,0.174,2.022,0.179],[121.7,0.138,1.29,0.258],[213.2,0.13,0.525,0.474],[104.6,0.17,0.729,0.244],[152.9,0.119,0.955,0.305],[97.1,0.145,1.768,0.176],[140.2,0.119,1.369,0.279],[187.5,0.111,0.752,0.363],[219.4,0.124,0.594,0.485],[168,0.132,0.697,0.375],[146.5,0.121,1.216,0.301],[133.9,0.12,1.33,0.266],[135.7,0.152,0.736,0.32],[126,0.123,2.067,0.258],[89,0.171,1.628,0.188],[124.4,0.154,0.785,0.287],[124.1,0.125,3.501,0.252],[180.1,0.116,1.025,0.374],[105.2,0.148,1.666,0.218],[173.1,0.115,0.845,0.345],[209,0.127,0.486,0.457],[93.8,0.153,7.832,0.191],[116,0.134,2.735,0.231],[179.8,0.114,0.721,0.363],[112.2,0.145,2.043,0.234],[189.7,0.122,0.758,0.411],[99.7,0.153,1.233,0.207],[130.3,0.127,0.923,0.251],[178.6,0.116,0.794,0.369],[110.5,0.145,1.733,0.234],[187.8,0.114,0.78,0.385],[163.5,0.157,0.563,0.407],[141.3,0.116,1.507,0.273],[119.9,0.14,1.979,0.253],[116,0.132,2.352,0.231],[157,0.133,0.81,0.356],[105.9,0.138,2.216,0.208],[170.2,0.117,1.137,0.334],[213.3,0.11,0.731,0.437],[116.6,0.132,1.816,0.227],[101.4,0.144,1.163,0.204],[120.1,0.145,0.78,0.267],[91.8,0.167,1.889,0.192],[94.4,0.162,1.679,0.201],[91.3,0.154,10.47,0.181],[146,0.113,6.124,0.306],[99.2,0.143,6.394,0.196],[130.4,0.128,1.361,0.265],[108.7,0.14,1.273,0.222],[96.7,0.157,1.903,0.2],[141.2,0.122,1.207,0.285],[137.3,0.126,1.31,0.267],[172.2,0.122,0.682,0.358],[227.1,0.112,0.615,0.463],[177.6,0.12,0.627,0.382],[228.7,0.11,0.625,0.471],[104.6,0.145,2.29,0.215],[142.5,0.123,1.079,0.287],[232.8,0.109,0.687,0.487],[126.5,0.138,1.024,0.27],[90.3,0.168,1.102,0.196],[93.7,0.156,2.769,0.188],[101.3,0.14,2.562,0.194],[197.4,0.116,0.688,0.411],[99.2,0.156,1.638,0.209],[109,0.138,1.155,0.22],[147.4,0.157,0.513,0.357],[174.4,0.115,0.818,0.342],[165.5,0.115,0.972,0.336],[126.3,0.143,1.405,0.278],[186.9,0.139,0.551,0.439],[179,0.112,0.988,0.37],[119.3,0.13,1.777,0.235],[93.7,0.175,0.844,0.213],[165.9,0.127,0.586,0.356],[120.9,0.14,0.748,0.25],[180,0.115,0.841,0.371],[101.8,0.14,1.925,0.205],[111.3,0.135,2.335,0.223],[128.1,0.139,1.364,0.278],[87.2,0.175,1.678,0.186],[238.7,0.109,0.66,0.471],[163,0.118,1.049,0.336],[111.1,0.158,2.101,0.247],[112.5,0.146,1.129,0.238],[100,0.163,1.083,0.224],[109.2,0.141,3.02,0.219],[166.8,0.117,0.973,0.345],[91.5,0.164,1.896,0.193],[111.7,0.141,1.232,0.232],[175.1,0.112,0.934,0.351],[148,0.122,1.027,0.304],[132.8,0.13,1.84,0.277],[172.5,0.13,0.9,0.382],[148.7,0.141,0.707,0.33],[93.4,0.157,2.727,0.18],[262.8,0.106,0.601,0.537],[245.3,0.114,0.555,0.501],[109.4,0.14,1.884,0.223],[91.4,0.164,3.359,0.177],[111.2,0.14,1.151,0.218],[122.7,0.147,0.773,0.276],[179.6,0.108,0.712,0.359],[93.3,0.157,7.356,0.188],[204.6,0.116,0.653,0.424],[141.5,0.132,1.148,0.306],[103.8,0.147,1.802,0.198],[98.2,0.154,1.882,0.201],[105,0.139,2.643,0.207],[96.8,0.147,2.976,0.197],[90.6,0.164,1.544,0.191],[106.8,0.14,2.978,0.208],[165.7,0.123,0.639,0.356],[97.9,0.147,1.226,0.193],[91.6,0.149,1.95,0.178],[119.1,0.152,1.255,0.269],[128.4,0.143,1.226,0.284],[95.2,0.147,6.599,0.184],[120.3,0.142,1.059,0.262],[86.7,0.167,3.21,0.172],[103.7,0.14,3.074,0.212],[140,0.129,1.315,0.294],[88.2,0.154,5.401,0.174],[113.9,0.151,1.348,0.25],[201,0.112,0.699,0.407],[97.6,0.177,0.902,0.22],[88.8,0.159,5.456,0.167],[214.8,0.115,0.67,0.455],[100,0.145,3.57,0.196],[93.2,0.156,6.131,0.184],[107.6,0.137,2.004,0.21],[115.6,0.134,1.726,0.227],[102.1,0.148,1.617,0.213],[118,0.135,1.415,0.228],[98.7,0.16,1.349,0.214],[100,0.147,1.508,0.202],[172.7,0.11,1.227,0.349],[104.3,0.173,0.717,0.243],[223,0.107,0.997,0.495],[173.5,0.123,0.813,0.369],[117.7,0.137,1.584,0.246],[211.3,0.109,0.714,0.438],[167.4,0.133,0.539,0.374],[164.8,0.12,0.979,0.34],[241.3,0.117,0.485,0.47],[130,0.124,1.118,0.259],[87.6,0.162,6.888,0.17],[118,0.142,1.192,0.255],[230.9,0.113,0.548,0.455],[185.1,0.125,0.636,0.404],[127.4,0.137,1.109,0.278],[108.5,0.151,1.233,0.235],[204.7,0.112,0.678,0.415],[123.6,0.141,1.149,0.27],[132.1,0.152,0.89,0.3],[156.6,0.125,0.872,0.326],[100.2,0.16,1.405,0.216],[85.6,0.187,1.635,0.192],[214.9,0.119,0.645,0.456],[110.6,0.135,2.725,0.234],[163.1,0.122,0.845,0.33],[101.8,0.149,5.287,0.204],[177.1,0.121,1.013,0.377],[101.1,0.145,10.361,0.196],[84.6,0.168,3.033,0.172],[167.6,0.114,0.873,0.334],[99.7,0.169,1.12,0.225],[109.7,0.137,2.163,0.211],[267,0.11,0.561,0.55],[138.1,0.14,0.893,0.301],[86.3,0.165,1.478,0.171],[95.8,0.153,2.344,0.186],[107.1,0.178,1.443,0.252],[136.2,0.14,0.724,0.297],[140.4,0.126,1.064,0.28],[252.3,0.108,0.6,0.517],[133.9,0.131,0.918,0.284],[249.2,0.11,0.532,0.494],[121.5,0.136,1.378,0.236],[119.6,0.135,1.263,0.261],[133.5,0.123,1.403,0.262],[117.2,0.132,2.64,0.243],[86.5,0.174,3.297,0.184],[237.5,0.106,0.58,0.469],[193.5,0.134,0.588,0.445],[92.8,0.155,2.715,0.184],[97.5,0.148,3.035,0.186],[120,0.143,0.976,0.26],[147.8,0.148,0.656,0.347],[232.8,0.109,0.711,0.489],[143.7,0.132,0.85,0.309],[136.5,0.14,0.804,0.304],[144.1,0.118,1.126,0.282],[193.1,0.122,0.643,0.399],[96.3,0.165,1.318,0.207],[121.3,0.143,1.185,0.267],[196,0.129,0.686,0.435],[90.7,0.16,5.377,0.185],[203.4,0.118,0.825,0.436],[155.6,0.137,0.587,0.353],[100.8,0.17,1.065,0.235],[172.9,0.125,0.806,0.372],[101.7,0.144,2.353,0.2],[139.3,0.124,1.102,0.274],[96,0.158,1.351,0.2],[141.9,0.133,1.06,0.309],[209,0.125,0.612,0.465],[114.8,0.141,1.455,0.242],[97.1,0.167,0.992,0.218],[88.5,0.178,1.383,0.194],[116.7,0.177,0.513,0.285],[112.1,0.144,3.674,0.229],[94.1,0.148,5.325,0.186],[133.3,0.13,0.838,0.278],[159.6,0.132,0.69,0.349],[119.2,0.134,0.935,0.24],[183,0.121,0.801,0.383],[107.9,0.147,1.525,0.226],[157.7,0.117,1.114,0.322],[145.6,0.121,1.202,0.289],[130.3,0.129,1.163,0.261],[99.7,0.154,2.466,0.21],[105.4,0.139,3.171,0.207],[140,0.151,0.603,0.327],[152,0.118,1.212,0.308],[136.7,0.138,0.938,0.297],[109.3,0.138,2.572,0.214]],"EC":[[632.3,0.0808,6.29,1.712],[260.5,0.083,13.11,0.71],[280.9,0.0903,3.16,0.692],[312.1,0.0867,4.23,0.813],[148.3,0.1004,22.72,0.441],[316.2,0.0772,6.77,0.773],[345.6,0.0703,3.4,0.837],[512.5,0.0795,1.55,1.383],[405.9,0.0827,1.94,1.068],[187.9,0.0876,8.98,0.56],[913.5,0.0676,4.17,2.175],[498.8,0.075,1.79,1.26],[234.5,0.0971,1.79,0.688],[302,0.0833,12.27,0.821],[628.3,0.068,1.72,1.548],[350.1,0.0688,3.71,0.895],[312.7,0.0862,1.9,0.899],[556,0.069,6.91,1.357],[550.9,0.068,2.04,1.355],[224.7,0.0957,5.93,0.652],[276.4,0.0729,18.92,0.724],[215.9,0.0798,3.59,0.535],[349.1,0.0737,1.67,0.86],[132.6,0.1183,6.76,0.435],[232.7,0.0787,28.07,0.602],[220.1,0.0815,25.58,0.566],[289,0.0781,2.78,0.749],[604,0.0804,24.84,1.556],[309.2,0.0749,1.37,0.894],[883.7,0.0805,2.67,2.491],[840.6,0.0697,2.62,1.983],[529.6,0.0726,10.23,1.37],[156.1,0.0815,6.78,0.442],[526.5,0.0683,2.21,1.251],[278.4,0.0773,5.75,0.727],[442.1,0.0625,2.84,1.159],[359.4,0.0698,2.82,0.923],[226.9,0.0982,6.63,0.619],[392.7,0.0607,9.79,1.067],[397.4,0.0789,2.6,1.063],[380.9,0.0764,9.86,1.004],[282.1,0.0686,11.64,0.725],[333.4,0.0849,6.86,0.853],[1224.7,0.0552,3.54,2.805],[288.9,0.0782,2.07,0.903],[322.5,0.0686,3.86,0.911],[276.4,0.0754,21.99,0.718],[299.4,0.0823,5.45,0.803],[252,0.0806,11.6,0.706],[396.6,0.0816,3.75,1.106],[300.3,0.0931,1.5,0.979],[367,0.0877,1.28,1],[313.1,0.0779,7.89,0.84],[328.6,0.0901,1.21,1.034],[287.5,0.0853,27.68,0.857],[475.9,0.0733,4,1.162],[268.5,0.0936,3.91,0.875],[195.6,0.0907,3,0.518],[386.7,0.0742,2.94,0.961],[298.5,0.0761,2.44,0.8],[192.1,0.0794,19.51,0.474],[1900.4,0.0773,6.66,4.787],[392.9,0.0733,1.53,1.025],[352.4,0.0809,9.51,0.967],[365.5,0.0879,1.27,1.122],[158.8,0.0978,4.28,0.432],[285.6,0.0889,1.5,0.889],[230,0.0759,2.75,0.653],[694.2,0.0757,2.28,1.743],[546.5,0.0761,1.7,1.365],[366.6,0.0778,4.92,0.998],[392.6,0.0721,4.34,1.008],[333.8,0.0816,4.29,0.886],[311.5,0.0803,5.3,0.934],[224.7,0.0987,5.77,0.553],[346.8,0.0922,1.46,0.966],[321.4,0.0885,1.21,0.871],[178.1,0.1031,46.31,0.533],[551,0.0645,3.4,1.391],[287.4,0.0789,9.32,0.784],[182.5,0.0937,10.31,0.522],[182.9,0.0938,3.58,0.501],[504.4,0.0789,1.38,1.324],[200.2,0.0846,5,0.675],[316.1,0.0765,19.26,0.853],[363.3,0.0818,3.14,0.898],[389.6,0.0746,2.29,1.03],[385.8,0.086,1.36,1.071],[395.4,0.0759,15.47,0.975],[233.3,0.0893,17.53,0.692],[603.4,0.069,8.27,1.521],[331.9,0.0798,2.63,0.889],[247.4,0.0937,4.21,0.693],[270.5,0.0695,7.64,0.681],[304.2,0.0825,15.53,0.867],[413.9,0.0791,2.82,1.087],[535.7,0.0698,1.67,1.388],[310.9,0.0738,2.15,0.797],[226.8,0.084,3.15,0.639],[258.1,0.0841,8.45,0.685],[314.7,0.0864,1.28,0.986],[365.5,0.0723,4.68,0.913],[196.4,0.0876,11.3,0.562],[451.7,0.0785,3.74,1.185],[320.1,0.0748,6.3,0.749],[314.5,0.0765,13,0.815],[176,0.0979,3.44,0.525],[555.8,0.0813,2.85,1.465],[181.2,0.0967,3.93,0.589],[329.5,0.0761,5.12,0.82],[359.4,0.0653,2.5,0.89],[591.8,0.0656,2.25,1.445],[252.7,0.0906,2.61,0.705],[281.8,0.0898,1.37,0.78],[374.5,0.0748,1.77,1.004],[405.7,0.0789,1.38,1.086],[364.3,0.0783,2.12,0.942],[365.8,0.08,4.13,0.943],[290.8,0.0811,25.98,0.737],[295.9,0.0972,2.26,0.897],[256.7,0.0833,20.47,0.743],[163.6,0.0873,9.66,0.452],[258.9,0.081,28.77,0.794],[478.4,0.0738,1.35,1.309],[328.1,0.0782,14.32,0.822],[279.6,0.082,36.21,0.776],[250.5,0.0825,3.77,0.624],[188.9,0.0789,7.94,0.507],[267.8,0.0893,4.1,0.767],[427.7,0.0664,14.98,0.996],[891.1,0.0726,3.45,2.133],[517.1,0.0754,1.39,1.243],[212.6,0.0892,3.13,0.595],[237.6,0.0803,3.89,0.637],[241.1,0.0882,1.95,0.73],[488,0.0652,6.31,1.287],[539.1,0.0803,1.58,1.293],[317.1,0.0683,3.12,0.785],[214.6,0.0885,6.18,0.605],[501.8,0.0665,1.8,1.19],[670.1,0.0678,2.71,1.701],[711.8,0.0748,2.3,1.869],[342.7,0.0716,3.12,0.864],[244.4,0.0865,5.65,0.693],[698.4,0.0741,3.04,2.039],[303.7,0.0784,1.43,0.869],[455.4,0.0694,1.56,1.079],[282.7,0.087,2.66,0.719],[478.5,0.0703,2.47,1.154],[371.1,0.081,1.17,1.06],[246.4,0.0744,3.04,0.751],[248.5,0.0896,1.9,0.73],[392.7,0.0899,1.69,1.132],[919,0.0774,18.92,2.204],[454.1,0.0745,3.31,1.251],[142.7,0.0937,10.92,0.397],[308.4,0.0753,3.64,0.82],[322.4,0.0744,1.86,0.766],[386.6,0.081,6.18,1.066],[509.1,0.0728,2.39,1.303],[456.3,0.0707,2.16,1.155],[344.8,0.0811,18.78,0.882],[416.3,0.0742,43.22,0.963],[506.3,0.0691,2.08,1.242],[228.8,0.082,53,0.669],[524.1,0.0669,2.23,1.331],[295.1,0.0755,11.83,0.806],[532.9,0.0711,1.34,1.333],[231,0.0734,16.77,0.625],[212,0.0784,1.51,0.605],[331.9,0.084,2.32,0.963],[346.6,0.0708,9.41,0.874],[466.2,0.0707,1.44,1.167],[315.5,0.0823,1.98,0.877],[372.2,0.0728,6.12,0.963],[324,0.0817,1.56,0.854],[299.6,0.0814,1.34,0.913],[321.7,0.0841,1.64,0.937],[118.3,0.1044,9.24,0.334],[317.6,0.0745,7.67,0.756],[402.7,0.0873,2.07,1.254],[163.5,0.0955,9.76,0.452],[426.7,0.0825,3.07,1.098],[310.4,0.0781,1.5,0.842],[302.2,0.076,2.42,0.776],[338.9,0.0808,1.1,0.987],[326,0.077,4.97,0.815],[169.6,0.0874,8.53,0.463],[330,0.0764,7.11,0.95],[501,0.0674,15.19,1.294],[442.7,0.0861,1.49,1.243],[211.1,0.081,14.22,0.607],[199.9,0.082,6,0.554],[357.2,0.0747,1.88,0.981],[550.7,0.0836,1.82,1.654],[256.9,0.0873,3.88,0.697],[261.6,0.078,2.03,0.721],[358.1,0.0809,6.74,0.976],[271.8,0.0834,3.44,0.736],[300.2,0.0762,24.19,0.762],[424.7,0.0756,1.67,1.084],[215.7,0.0859,8.82,0.592],[426.6,0.0804,2.02,1.161],[205.8,0.0924,21.74,0.586],[121.4,0.103,41.86,0.402],[214.1,0.0801,6.84,0.592],[473.8,0.0799,1.86,1.285],[302.1,0.0803,2.17,0.815],[211.2,0.0893,9.37,0.597],[446.4,0.0754,4.09,1.256],[294.4,0.0871,9.97,0.775],[266.8,0.0738,3.13,0.652],[415.5,0.0777,6.15,1.078],[697.4,0.0717,2.03,1.862],[378.8,0.0745,1.69,1.042],[719.5,0.0763,5.76,1.87],[236.6,0.0758,30.69,0.663],[348.6,0.0735,4.88,0.964],[208,0.0849,2.91,0.561],[388.5,0.0786,2.3,1.005],[329.3,0.0718,4.35,0.797],[208.3,0.0932,1.01,0.636],[372.8,0.078,9,0.971],[406.3,0.0833,3.09,1.16],[156.5,0.1048,15.9,0.469],[333.2,0.0705,3.65,0.937],[299.7,0.0894,1.8,0.759],[346.5,0.0891,1.86,0.953],[619.3,0.063,8.01,1.507],[592.3,0.0665,1.91,1.493],[589,0.0746,1.82,1.476],[392.7,0.0817,1.3,1.155],[270.5,0.0646,9.62,0.693],[499.2,0.0787,1.34,1.326],[409.5,0.0782,1.93,1.146],[210.9,0.0916,1.89,0.622],[350.8,0.0823,2.79,0.989],[499.1,0.0626,5.2,1.159],[775.5,0.0813,4.11,1.984],[1293.7,0.0764,3.87,3.52],[584.2,0.0713,1.49,1.366],[251.8,0.0779,5.26,0.635],[283.3,0.0895,1.14,0.822],[258.8,0.077,2.14,0.691],[317.9,0.0684,9.22,0.863],[257.7,0.0862,2.48,0.798],[474.9,0.0755,1.27,1.232],[644.3,0.0667,11.88,1.663],[312.7,0.0828,4.33,0.875],[508.3,0.0747,5,1.27],[277.5,0.0826,2.6,0.733],[359.2,0.0673,4.6,0.864],[160.9,0.0915,9.09,0.445],[245.3,0.0811,7.5,0.672],[431.8,0.0746,8.04,1.189],[622.7,0.0767,3.66,1.571],[362.9,0.0812,1.15,1.05],[222.2,0.0809,4.19,0.596],[264.9,0.0861,5.6,0.725],[362.7,0.076,4.5,0.963],[395.4,0.0788,1.66,1.04],[456.6,0.0678,6.38,1.071],[241.6,0.0848,9.26,0.641],[393.9,0.07,1.23,1.072],[668.7,0.0631,2.29,1.585],[332.8,0.0721,3.78,0.913],[211.9,0.0791,13.44,0.506],[415.1,0.0836,4.03,1.091],[372.4,0.0845,2.03,0.975],[237.2,0.0868,12.1,0.669],[399.4,0.0766,3.11,1.119],[319.6,0.0809,1.32,0.88],[252.6,0.0843,5.85,0.677],[254.2,0.084,2.39,0.631],[396.2,0.0775,2.46,1.045],[530.8,0.0846,3.06,1.507],[662.3,0.0691,3.62,1.526],[208.5,0.0848,3.85,0.591],[228,0.0887,4.58,0.62],[411,0.0807,14.87,1.053],[377.7,0.0862,4.06,0.968],[528.7,0.0757,1.92,1.381],[290.1,0.089,1.18,0.982],[259.1,0.0846,2.34,0.701],[319.5,0.0841,3.47,0.877],[161.3,0.0915,9.26,0.454],[347.4,0.0733,1.47,1.041],[247.3,0.0986,19.9,0.68],[349.5,0.0765,6.66,0.961],[194.3,0.103,2.93,0.75],[514.6,0.0573,5.75,1.17],[506.1,0.0817,9.44,1.277],[484.7,0.0801,1.51,1.347],[423.4,0.0787,1.61,1.124],[294,0.0792,4.16,0.831],[255.9,0.0919,8.88,0.747],[283.2,0.093,1.05,0.896],[395.4,0.0731,6.24,0.995],[318.1,0.0891,1.02,0.898],[461.5,0.0671,5.8,1.118],[471,0.0788,2.2,1.219],[288.5,0.0606,41.21,0.702],[614.1,0.0657,1.95,1.565],[207.7,0.0928,6.67,0.576],[345.4,0.0799,5.36,0.858],[394.2,0.0883,1.53,1.052],[249.8,0.0823,10.93,0.688],[357.4,0.0797,3.73,0.959],[715,0.0717,1.99,1.675],[281.4,0.0816,1.71,0.759],[559.2,0.0771,13.75,1.359],[617.8,0.0613,8.85,1.514],[417.2,0.0742,8.16,1.138]],"EB":[[1013.9,0.777,39.3,2.64],[1514.1,0.759,57.8,3.82],[1364.2,0.672,40.5,3.54],[2236.3,0.679,52.1,5.36],[1439.6,0.687,29.2,3.5],[2103.4,0.615,32.5,4.41],[2347.5,0.669,126.7,5.14],[1139.3,0.77,51.4,2.84],[1535.1,0.869,38,4.2],[1599.3,0.769,27.2,3.83],[2037.4,0.715,45,4.69],[2045.2,0.629,40.5,4.51],[2338.7,0.664,76.9,5.17],[1888.5,0.724,25.4,4.71],[1002.9,0.82,69.4,2.83],[1788.6,0.702,70.4,4.13],[1675.9,0.736,23.8,3.89],[1529.6,0.741,95.5,3.76],[2301.3,0.589,27.4,4.69],[2115.9,0.641,26.6,4.45],[2595.6,0.693,62.6,5.66],[2317.2,0.655,32.1,5.34],[3384.2,0.713,78.2,8.7],[2772.9,0.78,89.6,7.35],[2055.6,0.733,56.3,5.3],[1704.7,0.708,72.8,4.24],[2609,0.601,71.3,5.46],[2079.6,0.674,125.7,5.29],[3500.9,0.725,65.7,9.02],[3457.4,0.658,114,8.59],[3023.4,0.748,65.7,8.26],[1307.5,0.724,36.5,3.26],[1398.4,0.773,28.2,3.48],[1363.8,0.696,56.8,3.38],[1802.2,0.665,47.7,4.2],[1531.4,0.719,37.5,3.82],[1809.1,0.726,62.1,4.28],[2307.8,0.623,69.3,4.73],[2365.5,0.612,52.5,5.02],[2565.9,0.663,18.2,5.63],[1852.5,0.632,42.9,3.96],[1287.9,0.769,43.9,3.18],[1814.1,0.695,22.9,4.38],[2172.1,0.685,33.6,4.83],[1825.3,0.62,32.3,3.83],[2048.6,0.641,48.6,4.41],[1791.2,0.666,44.7,3.92],[1364.2,0.789,20,3.47],[1499.2,0.714,65.3,3.52],[2086.9,0.598,31.9,4.46],[2667,0.582,14.3,5.41],[1377.2,0.832,81,3.42],[1569.1,0.67,43.7,3.45],[2140.8,0.607,56.4,4.52],[2064.2,0.615,23.2,4.26],[2212.5,0.582,50.3,4.53],[2348.5,0.611,40.5,4.82],[2411,0.616,41,4.91],[2463.7,0.621,42.2,5.05],[2442.1,0.615,42.1,5.01],[1630.7,0.717,58.3,3.67],[3082.3,0.681,56.2,6.79],[3269.7,0.713,70.6,8.92],[2583.8,0.708,50.2,6.7],[2294.5,0.659,55,5.1],[2763.7,0.604,53.4,5.82],[2243.1,0.683,52.7,5],[1575.7,0.656,26.7,3.5],[1796.4,0.612,46.4,3.71],[1707.1,0.758,56.7,3.92],[1620.8,0.734,15.1,4.1],[1719.2,0.64,49,3.82],[2397.2,0.654,40.1,5.26],[1496.4,0.851,56.5,3.66],[2075.5,0.666,76.9,4.35],[2243,0.682,58.3,5.15],[1436.4,0.727,46.1,3.52],[3307,0.668,99.9,7.79],[3284.2,0.702,79.7,8.62],[2026.7,0.649,27.6,4.4],[2065.2,0.684,29.3,4.82],[2908.8,0.647,58.3,6.34],[1916.7,0.708,23,4.61],[2488.2,0.681,40.1,5.44],[2467.6,0.68,35.1,5.27],[2661.6,0.641,35.2,5.47],[1984.5,0.608,48,4.17],[1180.5,0.776,75.5,3.08],[1135,0.752,65.5,2.98],[2081.1,0.686,45,4.81],[1878.9,0.707,78.1,4.11],[2461.4,0.638,58.1,5.11],[3213.4,0.62,17.8,6.53],[3972.8,0.662,44.1,9.21],[2328.4,0.679,73.8,5.41],[1967.3,0.625,78,4.11],[3319.3,0.625,53,6.57],[1383.8,0.687,40,3.33],[1931.9,0.759,72.9,4.47],[2392.3,0.654,75,5.17],[1667.9,0.784,111.4,4.01],[2838.3,0.667,55.8,6.36],[1565.3,0.693,20.4,3.62],[1727.9,0.687,90.7,3.99],[2281.5,0.645,52,5.17],[1406.7,0.766,70.4,3.48],[1613.8,0.707,72.9,3.99],[1598.1,0.778,43.4,3.99],[1968.8,0.706,88.9,4.91],[1918.2,0.628,62.1,4.22],[1813,0.705,23.7,4.01],[1714.3,0.783,50.7,4.03],[2020.7,0.804,21,5.13],[2159.9,0.607,68,4.55],[1700.1,0.686,45.8,3.8],[1026.6,0.816,28.1,2.88],[2749.3,0.603,19.8,5.66],[1969.8,0.637,36.2,4.26],[2547.5,0.625,29.7,5.26],[2958,0.618,36.8,6.29],[2136.4,0.618,35.7,4.47],[1958,0.672,41.5,4.32],[1841.1,0.679,51.6,4.23],[2014.7,0.599,55.7,4.25],[1712.1,0.698,16.4,4.26],[1835,0.695,47.8,4.4],[1952.5,0.681,66.2,4.52],[1900.4,0.714,49.4,4.36],[2301.3,0.628,48.6,4.86],[2001.3,0.702,88.6,4.75],[1986.5,0.677,33,4.57],[2615.6,0.746,45.4,6.76],[988.1,0.803,66.1,2.73],[2048.8,0.64,74.5,4.3],[2782.4,0.586,64.5,5.53],[2405.4,0.663,81.1,5.3],[2693.2,0.657,50.1,6.08],[1818,0.749,57.3,4.21],[1778.4,0.749,41.9,4.26],[3004.1,0.636,57.5,6.45],[2331.8,0.636,50.3,5],[1964.9,0.661,53.9,4.49],[2928.3,0.614,69.4,5.83],[2042.2,0.681,30.4,4.74],[2041.6,0.639,25.2,4.67],[2565,0.582,43.6,5.11],[1749.5,0.676,24.3,3.77],[3283.4,0.572,18.3,6.62],[2759.3,0.587,44.8,5.67],[1541.1,0.726,13.2,3.77],[1537.5,0.881,36.2,3.91],[1396.4,0.745,58.8,3.42],[2612.4,0.594,25.3,5.25],[1774.5,0.666,31.2,3.96],[1824.9,0.676,84.4,4.31],[1522,0.672,23.3,3.48],[1585.7,0.747,53.5,4.05],[1167.9,0.649,128.7,2.89],[1800.4,0.727,42.8,4.22],[1867.5,0.644,61.7,4.03],[3487.6,0.65,27.4,7.4],[1826.7,0.647,49.2,3.95],[1520.5,0.708,71.7,3.67],[1184.6,0.804,60.9,3.06],[2551.6,0.645,18.8,5.45],[1963.7,0.701,52.7,4.83],[3847.7,0.7,71.6,9.57],[2347.1,0.654,31,5.23],[1386.8,0.7,62.8,3.15],[1867.2,0.68,106.1,4.17],[1805.3,0.715,45.8,4.34],[2370.4,0.684,48.9,5.58],[2071.6,0.727,117.7,4.99],[4295.8,0.678,51.8,10.01],[1711.4,0.763,127.8,4.35],[1852.3,0.651,29.8,3.88],[1787.4,0.626,31.3,3.76],[1765.2,0.636,31.7,3.75],[1717.3,0.625,30.5,3.64],[1865.3,0.631,29.9,3.92],[1866.2,0.628,29.8,3.91],[1831.7,0.623,30.1,3.88],[1932.9,0.614,30.3,3.99],[1900.4,0.617,29.9,4.01],[1929.9,0.637,29,4.09],[2058.5,0.624,30.9,4.31],[2026,0.642,30.2,4.29],[2051.4,0.626,31.2,4.28],[2039.5,0.644,31.9,4.27],[2105.1,0.634,31.2,4.37],[2112.2,0.601,32.2,4.36],[2179.5,0.61,31.7,4.47],[2150.5,0.616,31.5,4.44],[2155.8,0.622,31.4,4.45],[2243.3,0.609,32.3,4.58],[2220.8,0.607,32.6,4.55],[2129.3,0.628,33.9,4.46],[2145,0.614,33.7,4.44],[2188.2,0.625,33.6,4.51],[2172.9,0.62,33.8,4.51],[2161.1,0.616,34,4.49],[2221.8,0.601,32.9,4.55],[2267.5,0.587,33.2,4.63],[2248,0.595,32.6,4.58],[2210.3,0.596,32.4,4.53],[2249.9,0.603,32.6,4.57],[2198.5,0.589,33.5,4.46],[2207.3,0.583,33.6,4.47],[2219.7,0.582,33.9,4.48],[2254.8,0.595,33.8,4.58],[2229.5,0.594,33.8,4.53],[2205.2,0.595,33.7,4.47],[2284.7,0.576,33.7,4.61],[2235.5,0.576,33.2,4.52],[2237.2,0.594,33.2,4.56],[2205,0.607,32.8,4.52],[2223.1,0.605,33,4.55],[2255.9,0.581,33.7,4.51],[2251.7,0.569,34.9,4.58],[2287.3,0.564,35.3,4.65],[2251.3,0.621,33.2,4.68],[2228.5,0.609,33.5,4.62],[2205.7,0.621,34.5,4.6],[2206.4,0.589,34.3,4.56],[2179,0.603,35,4.42],[2211.2,0.583,34.8,4.5],[2244.2,0.602,32.6,4.67],[2290.6,0.617,32,4.7],[2959.5,0.734,85.5,7.73],[1765.8,0.664,61.6,3.84],[2345.2,0.592,43.1,4.91],[2722.8,0.595,36.9,5.46],[2429.5,0.594,8.3,5.18],[2284.5,0.613,30.5,4.87],[1941.7,0.645,54.1,4.2],[2298.1,0.645,54.1,5.21],[1558.5,0.753,34.2,3.64],[1002.6,0.836,47.1,2.7],[1651.1,0.69,29.8,3.68],[2052.2,0.686,53.4,4.47],[2714.8,0.719,76.2,6.76],[2042.3,0.618,45.9,4.36],[1777.4,0.63,42.1,3.71],[2143,0.626,40.8,4.56],[1315.3,0.689,24.2,3.3],[2264.7,0.668,19.6,4.93],[2104.4,0.687,22.9,4.77],[2390.3,0.619,44.4,4.89],[1945.9,0.674,47,4.32],[1569.3,0.777,55,3.83],[1542.3,0.712,36.2,3.92],[1603.8,0.716,31.9,3.9],[2265.7,0.714,74.4,5.29],[1776.1,0.743,61.5,4.04],[2459.6,0.699,26.2,5.47],[1603.3,0.759,44,3.82],[1830.6,0.697,65.1,4.16],[2057.9,0.678,82.9,4.54],[1374.2,0.688,84.2,3.26],[1532.3,0.772,80,3.62],[2694.7,0.653,66.8,5.91],[1149.4,0.734,82.2,2.88],[1477,0.726,77.2,3.66],[2572.5,0.609,40.6,5.15],[2816.9,0.667,44.3,6.16],[1928,0.679,28.9,4.39],[1489,0.738,35.6,3.46],[2184.9,0.68,29.7,4.88],[1530.5,0.681,53.7,3.63],[1841.9,0.617,31.7,3.89],[2168.6,0.651,50.2,5.08],[2120.4,0.682,57.8,4.72],[2112.5,0.733,51.6,5.06],[1348.8,0.701,32.5,3.34],[2066,0.674,39.3,4.95],[1667.7,0.737,43.8,3.71],[1342.2,0.747,64.1,3.38],[2412,0.61,41.7,5.09],[1859.5,0.648,83.7,3.91],[1952.6,0.705,46.8,4.24],[2001.9,0.613,65.7,4.44],[1875.6,0.671,22.1,4.15],[1877.1,0.632,29.1,3.99],[1721.3,0.643,54,3.72],[2475.5,0.641,82.9,5.22],[989.9,0.829,41.6,2.68],[1635.1,0.729,39.6,3.84],[2659.2,0.641,65.5,5.81],[2816.2,0.633,37.7,5.82],[2342.2,0.721,34.9,5.47],[1979.5,0.635,65.7,4.3],[2124.6,0.663,24,5.11],[1641.1,0.631,46.3,3.59],[1994.1,0.634,27.8,4.56],[2076.2,0.667,24.6,4.57],[1969.8,0.637,30.1,4.27],[3567.9,0.678,41.4,7.9],[2182.9,0.665,58,4.67],[2123.9,0.613,107.9,4.34],[2168.3,0.668,51.2,4.69],[2497.6,0.667,68.8,5.4],[1520,0.678,30.6,3.47],[1685.4,0.71,52.7,3.74],[962.2,0.77,45.9,2.66],[2085.7,0.634,41.3,4.39],[2224.9,0.622,41.7,4.61],[1602,0.65,18.6,3.63],[1031.4,0.893,36.2,2.78],[1913.5,0.701,42.2,4.37],[2498.6,0.615,32.2,5.37],[2525.8,0.632,26.2,5.39],[1564,0.688,40,3.62],[1541.2,0.763,41,3.78]],"EUCS":[[14.988,0.061,0.031,0.112],[15.7,0.054,0.02,0.107],[20.142,1.476,0.008,0.095],[11.983,0.047,0.033,0.088],[20.954,0.02,0.023,0.1],[13.205,0.077,0.023,0.093],[12.038,0.074,0.034,0.087],[13.162,0.038,0.028,0.085],[14.578,0.049,0.027,0.098],[12.463,0.076,0.023,0.098],[27.825,0.011,0.029,0.108],[13.17,1.413,0.014,0.08],[15.927,0.042,0.034,0.111],[16.095,0.031,0.033,0.102],[21.081,3.262,0.012,0.131],[18.623,0.043,0.023,0.12],[13.461,0.05,0.038,0.11],[31.378,0.256,0.005,0.106],[15.952,2.322,0.015,0.102],[12.669,0.047,0.043,0.097],[14.11,0.046,0.031,0.097],[25.63,0.261,0.006,0.099],[8.187,4.51,0.023,0.088],[20.341,0.017,0.026,0.097],[20.123,0.018,0.025,0.096],[18.969,0.024,0.02,0.095],[18.467,0.022,0.022,0.094],[17.92,0.022,0.023,0.094],[18.213,0.02,0.028,0.095],[18.016,0.018,0.027,0.095],[17.358,0.021,0.026,0.095],[14.855,0.069,0.024,0.105],[13.403,1.132,0.024,0.097],[13.56,0.872,0.022,0.103],[12.84,0.516,0.038,0.097],[18.806,0.022,0.022,0.09],[12.462,0.046,0.034,0.094],[21.764,2.22,0.008,0.096],[13.753,0.126,0.026,0.103],[13.413,3.583,0.016,0.098],[16.774,0.807,0.009,0.097],[24.617,4.535,0.006,0.087],[16.033,0.558,0.017,0.104],[16.087,0.034,0.025,0.098],[29.355,2.322,0.005,0.108],[28.738,1.234,0.007,0.133],[30.224,3.837,0.005,0.098],[22.338,0.429,0.006,0.089],[22.303,5.113,0.008,0.104],[33.77,4.332,0.005,0.12],[15.671,0.188,0.012,0.088],[18.455,0.021,0.024,0.1],[12.94,0.059,0.025,0.092],[13.757,0.206,0.035,0.109],[13.066,1.52,0.034,0.106],[18.042,4.195,0.026,0.133],[11.745,3.212,0.023,0.106],[10.489,0.123,0.026,0.095],[13.386,0.073,0.028,0.098],[11.132,1.153,0.037,0.086],[16.936,0.121,0.024,0.126],[11.536,3.603,0.023,0.091],[26.869,0.112,0.007,0.108],[30.423,1.991,0.005,0.101],[28.588,0.379,0.006,0.108],[15.859,0.095,0.013,0.092],[11.502,4.007,0.03,0.087],[16.909,0.019,0.035,0.093],[27.717,3.016,0.008,0.125],[25.508,3.707,0.007,0.113],[15.636,3.237,0.028,0.127],[13.862,3.023,0.025,0.098],[19.799,0.783,0.01,0.105],[23.79,4.069,0.009,0.114],[22.636,0.216,0.008,0.103],[12.81,0.064,0.048,0.107],[21.913,3.434,0.007,0.096],[12.209,5.006,0.021,0.1],[27.721,0.326,0.005,0.094],[13.036,0.037,0.035,0.095],[19.349,0.017,0.028,0.092],[51.672,0.705,0.005,0.158],[9.718,2.676,0.029,0.095],[11.551,2.204,0.035,0.098],[13.799,0.038,0.036,0.094],[11.935,0.352,0.021,0.09],[14.833,4.155,0.016,0.097],[11.069,1.248,0.033,0.092],[24.274,1.269,0.008,0.097],[10.525,0.124,0.024,0.09],[11.62,4.912,0.026,0.094],[13.753,2.85,0.029,0.103],[11.972,5.133,0.035,0.09],[12.746,0.043,0.033,0.09],[12.895,0.048,0.028,0.088],[24.496,4.318,0.006,0.097],[14.497,0.073,0.028,0.109],[13.662,0.04,0.028,0.09],[20.214,2.071,0.008,0.104],[12.437,3.262,0.032,0.099],[17.77,0.033,0.029,0.113],[14.673,0.055,0.045,0.105],[13.598,0.04,0.029,0.09],[14.561,0.038,0.025,0.092],[19.215,0.024,0.023,0.1],[16.479,0.028,0.031,0.096],[16.199,0.031,0.024,0.098],[21.942,2.313,0.007,0.099],[14.023,0.071,0.028,0.101],[15.109,0.245,0.023,0.11],[16.392,0.044,0.028,0.114],[14.362,0.946,0.015,0.096],[9.792,4.172,0.034,0.084],[26.557,5.088,0.006,0.102],[16.433,0.107,0.016,0.099],[30.793,2.89,0.005,0.108],[18.695,5.783,0.011,0.105],[19.216,0.299,0.009,0.093],[24.167,5.855,0.006,0.092],[14.097,2.19,0.032,0.101],[12.859,0.081,0.026,0.095],[17.197,0.032,0.026,0.102],[17.617,0.043,0.025,0.115],[13.161,0.045,0.027,0.087],[16.435,0.039,0.025,0.1],[8.068,3.971,0.029,0.095],[10.362,4.786,0.027,0.112],[15.401,3.107,0.014,0.098],[11.246,0.29,0.026,0.086],[14.504,1.834,0.027,0.109],[19.237,0.02,0.023,0.097],[15.395,0.037,0.026,0.101],[15.048,0.043,0.024,0.097],[16.107,0.026,0.029,0.096],[11.428,4.87,0.035,0.092],[17.475,4.242,0.012,0.1],[24.829,1.825,0.007,0.109],[25.376,4.484,0.007,0.104],[15.796,0.135,0.012,0.087],[15.273,2.345,0.011,0.088],[29.983,4.647,0.005,0.098],[10.894,0.339,0.029,0.094],[14.786,0.032,0.026,0.089],[22.007,0.022,0.02,0.113],[12.345,0.099,0.034,0.097],[20.947,0.026,0.022,0.11],[23.839,0.014,0.028,0.11],[14.626,0.053,0.023,0.099],[33.362,5.544,0.006,0.117],[17.002,0.085,0.031,0.135],[22.318,0.016,0.027,0.106],[17.435,0.051,0.014,0.088],[14.166,0.078,0.028,0.104],[16.368,0.027,0.024,0.1],[12.001,0.045,0.024,0.078],[11.402,0.14,0.03,0.087],[28.256,2.037,0.006,0.108],[30.776,2.95,0.005,0.115],[13.383,3.65,0.015,0.098],[15.929,0.035,0.032,0.107],[34.29,5.691,0.007,0.127],[15.874,0.041,0.027,0.106],[12.981,0.053,0.047,0.09],[12.853,5.731,0.023,0.103],[24.779,1.478,0.006,0.095],[19.832,0.527,0.007,0.083],[26.97,5.188,0.006,0.102],[23.215,4.436,0.014,0.141],[15.958,3.171,0.012,0.092],[17.429,0.029,0.023,0.101],[14.188,0.084,0.027,0.102],[14.529,0.037,0.026,0.094],[17.911,0.026,0.021,0.096],[14.322,0.063,0.029,0.105],[10.648,3.927,0.019,0.089],[17.019,1.823,0.022,0.111],[10.891,5.544,0.027,0.092],[12.366,0.096,0.025,0.088],[13.764,0.034,0.029,0.093],[29.047,0.564,0.007,0.113],[14.321,0.019,0.119,0.095],[12.18,5.703,0.026,0.106],[11.824,0.829,0.028,0.105],[25.789,0.764,0.005,0.094],[18.062,5.93,0.008,0.094],[24.666,1.988,0.006,0.091],[23.742,5.551,0.007,0.099],[20.341,0.873,0.012,0.128],[12.62,4.93,0.028,0.099],[14.594,2.036,0.011,0.09],[12.937,0.357,0.029,0.096],[9.905,0.513,0.022,0.088],[16.655,0.023,0.028,0.093],[14.187,4.847,0.017,0.099],[23.755,2.353,0.007,0.093],[16.612,0.061,0.016,0.098],[22.974,0.233,0.008,0.094],[16.395,1.696,0.016,0.115],[14.215,0.038,0.033,0.101],[28.575,0.005,0.133,0.099],[19.32,0.008,3.734,0.081],[22.152,0.008,3.811,0.098],[13.891,0.031,0.035,0.103],[18.021,0.032,0.027,0.118],[14.329,0.034,0.03,0.092],[15.598,0.034,0.026,0.096],[12.6,0.364,0.016,0.093],[14.869,0.038,0.028,0.102],[12.908,0.05,0.038,0.098],[17.308,0.038,0.021,0.1],[16.058,0.049,0.019,0.095],[21.763,0.018,0.03,0.112],[11.005,0.057,0.028,0.077],[11.985,2.386,0.021,0.104],[13.847,1.659,0.011,0.089],[22.65,3.121,0.008,0.106],[16.54,0.714,0.009,0.084],[17.057,4.939,0.018,0.133],[12.825,1.656,0.034,0.096],[15.315,0.051,0.025,0.104],[26.865,0.559,0.007,0.106],[23.707,4.806,0.006,0.114],[20.256,1,0.01,0.105],[28.925,2.964,0.006,0.119],[18.912,5.998,0.008,0.088],[21.227,0.013,0.091,0.119],[20.868,2.979,0.013,0.116],[16.727,0.407,0.015,0.101],[12.644,2.667,0.026,0.111],[13.018,0.056,0.037,0.095],[12.676,5.508,0.035,0.108],[13.246,0.059,0.028,0.096],[23.552,3.264,0.009,0.113],[12.863,0.039,0.032,0.089],[13.814,0.044,0.028,0.098],[15.187,0.336,0.017,0.114],[20.694,1.605,0.008,0.097],[22.772,4.757,0.006,0.097],[32.423,1.436,0.005,0.105],[29.24,5.147,0.005,0.099],[11.94,5.214,0.019,0.105],[24.51,0.659,0.007,0.103],[17.403,2.577,0.011,0.096],[16.222,1.173,0.009,0.089],[15.365,0.033,0.027,0.099],[26.007,5.528,0.008,0.118],[31.846,4.469,0.005,0.101],[27.195,3.016,0.005,0.099],[25.637,5.758,0.005,0.094],[26.211,1.555,0.005,0.085],[14.986,0.036,0.035,0.097],[29.139,2.141,0.005,0.085],[23.254,2.397,0.007,0.101],[23.393,1.661,0.009,0.111],[20.087,0.172,0.01,0.101],[18.574,0.034,0.024,0.112],[21.924,0.052,0.01,0.102],[26.049,0.014,0.028,0.113],[15.342,0.039,0.028,0.102],[28.634,2.271,0.005,0.1],[15.927,0.034,0.021,0.091],[21.283,2.395,0.008,0.089],[22.041,0.021,0.02,0.103],[17.543,0.028,0.027,0.104],[15.17,0.032,0.03,0.102],[16.217,0.039,0.026,0.106],[12.46,0.074,0.028,0.094],[17.618,0.026,0.032,0.106],[14.859,3.706,0.017,0.108],[14.554,0.036,0.028,0.095],[19.042,0.375,0.024,0.151],[20.343,4.754,0.01,0.102],[28.8,1.901,0.006,0.106],[28.722,4.7,0.005,0.095],[31.006,2.696,0.005,0.107],[12.738,2.795,0.028,0.098],[20.011,5.86,0.026,0.146],[17.098,3.336,0.026,0.133],[10.847,2.636,0.027,0.088],[13.656,2.225,0.021,0.107],[18.241,0.031,0.034,0.119],[16.247,0.023,0.033,0.099],[23.286,3.053,0.008,0.107],[27.148,3.133,0.005,0.087],[15.462,5.781,0.013,0.088],[17.039,5.645,0.015,0.112],[14.427,5.209,0.012,0.085],[15.613,0.03,0.028,0.101],[15.251,0.037,0.021,0.096],[14.149,4.424,0.032,0.114],[14.579,0.628,0.032,0.115],[12.853,0.055,0.027,0.092],[14.082,0.041,0.028,0.093],[15.934,0.042,0.025,0.103],[19.65,3.913,0.01,0.111],[11.594,0.036,0.05,0.099],[15.234,4.866,0.023,0.113],[31.828,0.409,0.005,0.104],[13.225,3.299,0.03,0.102],[10.683,4.506,0.034,0.096],[16.149,0.053,0.024,0.114],[16.409,0.044,0.021,0.103],[19.716,3.412,0.008,0.091],[16.11,0.043,0.029,0.109],[14.625,0.121,0.023,0.107],[26.315,5.836,0.005,0.095],[16.411,1.387,0.012,0.095],[33.44,3.397,0.005,0.119],[17.291,2.868,0.012,0.1],[32.648,1.249,0.005,0.102],[26.801,5.876,0.005,0.089],[14.011,4.926,0.018,0.096],[13.792,4.562,0.037,0.104]]} \ No newline at end of file diff --git a/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt b/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt index 31da8c2..98d295f 100644 --- a/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt +++ b/app/src/main/java/com/hormonetrack/HormoneTrackApp.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import com.hormonetrack.data.AppDatabase import com.hormonetrack.data.repository.HormoneRepository -import com.hormonetrack.pk.PKProfileStore +import com.hormonetrack.pk.EstrannaiseModels import com.hormonetrack.reminder.createNotificationChannel import com.hormonetrack.util.AppLog import com.hormonetrack.reminder.AlertNotifier @@ -19,7 +19,10 @@ class HormoneTrackApp : Application() { override fun onCreate() { super.onCreate() container = AppContainer(this) - PKProfileStore.init(this) + // v1.9.0 : les tables ODS ne sont plus chargées au démarrage (le + // runtime ne lit que des sources analytiques). L'asset MCMC + // (nuage d'incertitude ESE) les remplace (≈ 48 Ko). + EstrannaiseModels.init(this) createNotificationChannel(this) AppLog.init(this) // v1.4.2 : worker WorkManager PÉRIODIQUE des seuils d'alerte — diff --git a/app/src/main/java/com/hormonetrack/data/model/Treatment.kt b/app/src/main/java/com/hormonetrack/data/model/Treatment.kt index 04d8886..6a0188b 100644 --- a/app/src/main/java/com/hormonetrack/data/model/Treatment.kt +++ b/app/src/main/java/com/hormonetrack/data/model/Treatment.kt @@ -16,20 +16,30 @@ object Esters { const val EC = "EC" // cypionate, solution huileuse const val ECS = "ECS" // cypionate, suspension aqueuse (microcristaux) const val PEP = "PEP" // polyestradiol phosphate + // v1.9.0 : undécylate en SUSPENSION CRISTALLINE — couvert par le modèle + // Estrannaise ANALYTIQUE (estrannaise.js « EUn casubq ») ; ni TFS ni les + // tables ODS ne le couvrent. + const val EUCS = "EUCS" /** Esters injectables proposables par ester (override par dose). */ - val INJECTABLE: List = listOf(EV, EU, EEN, EB, EC, ECS, PEP) + val INJECTABLE: List = listOf(EV, EU, EEN, EB, EC, ECS, PEP, EUCS) /** * Esters proposables dans l'override par dose pour un MODÈLE PK donné * (v1.4.6, cf DoseDialog) : la liste suit la couverture du modèle — * TFS (V3C méta-analyse) → les 7 esters ; WHSAH (fit Mona) → les 6 - * (PEP non couvert par ce fit) ; Estrannaise (tables ODS) → EV/EU/EEn - * seulement (un traitement ESE + EB produirait 0 pg/mL). + * (PEP non couvert par ce fit) ; ESE (Estrannaise ANALYTIQUE v1.9.0) + * → les 6 injectables du fit estrannaise.js (dont EUCS, exclusif) ; + * les tables ODS historiques n'en couvraient que 3 (EV/EU/EEn). */ fun choicesForModel(pkModel: String): List = when (pkModel) { - PKModels.TRANSFEM_SCIENCE -> INJECTABLE + // ⚠️ TFS = liste EXPLICITE (fix v1.8.2-étendu) : INJECTABLE a gagné + // EUCS (couvert par l'Estrannaise analytique) mais le V3C TFS n'a PAS + // de paramètres pour EUCS → le dispatcher donnerait 0. Chaque liste + // reflète la couverture RÉELLE de son modèle. + PKModels.TRANSFEM_SCIENCE -> listOf(EV, EU, EEN, EB, EC, ECS, PEP) PKModels.WHSAH -> listOf(EV, EU, EEN, EB, EC, ECS) + PKModels.ESTRANNAISE -> listOf(EV, EU, EEN, EC, EB, EUCS) else -> listOf(EV, EU, EEN) } } diff --git a/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt b/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt new file mode 100644 index 0000000..1d78eb5 --- /dev/null +++ b/app/src/main/java/com/hormonetrack/pk/EstrannaiseCloud.kt @@ -0,0 +1,127 @@ +package com.hormonetrack.pk + +import com.hormonetrack.data.model.DoseLog +import com.hormonetrack.data.model.PKModels +import com.hormonetrack.data.model.Treatment +import com.hormonetrack.data.model.TreatmentType + +/** + * NUAGE D'INCERTITUDE (v1.9.0) — feature **exclusive au modèle ESE**. + * + * SOURCE : Estrannaise publie le **posterior MCMC** de ses paramètres + * (313 échantillons `(d, k1, k2, k3)` par ester injectable — cf + * [EstrannaiseModels]). Le graphique de simulation peut dessiner un + * **nuage diffus** : M courbes (une par échantillon échelonné) qui montrent + * la plage d'imprécision possible du modèle — exactement l'affichage du + * site estrannaise. TFS/WHSAH ne publient pas de posterior → pas de nuage + * pour eux (choix confirmé : le nuage reste sur ESE). + * + * QU'EST-CE QUE LE NUAGE REPRÉSENTE : la superposition des doses des + * traitements E2 **à profil ESE** évaluée avec UN échantillon MCMC par + * courbe — chaque polyligne du nuage est « ce que serait la courbe si le + * vrai paramètre physiologique était celui-là ». Les contributions des + * autres traitements (TFS/WHSAH/Bateman) ne sont PAS incluses dans le + * nuage : elles n'ont pas d'incertitude publiée (limite documentée — le + * nuage montre l'incertitude du modèle Estrannaise, pas le niveau total). + * + * DÉTERMINISME : les M échantillons sont **échelonnés** (stride sur les + * 313, pas d'aléatoire) — le nuage est identique à chaque affichage et + * testable en JVM (c'est aussi la reproducibilité du site, qui tire au + * hasard à chaque trace, lui). + * + * PUR (aucune dépendance Android) → testable en JVM (EstrannaiseCloudTest). + */ +object EstrannaiseCloud { + + /** + * Nombre de courbes du nuage : assez pour une plage visuellement + * « diffuse » (le site en trace des centaines, mais 32 échantillons + * échelonnés couvrent déjà l'étendue du posterior à un coût ×32 sur le + * calcul — le pas de la grille reste 1 h). + */ + const val DEFAULT_NB_CURVES = 32 + + /** + * Calcule le nuage d'incertitude. + * + * @param treatments TOUS les traitements (actifs ET inactifs, §6.bis) + * @param doseLogs toutes les doses réelles + * @param startMs/endMs fenêtre demandée (celle du graphique) + * @param stepMs pas de la grille (stepForRange) + * @param nowMs « maintenant » (les doses futures du porteur ne sont + * pas loguées — seules les doses à horizon prévisionnel + * alimentées par le caller seraient incluses ; v1.9.0 + * limite le nuage aux doses RÉELLES, comme le tracé labs) + * @param nbCurves nombre de courbes du nuage (défaut [DEFAULT_NB_CURVES]) + * @return M listes de points (M = [nbCurves]) — **vide** si rien n'est + * calculable : aucun traitement E2 actif à profil ESE avec un ester + * couvert, aucune dose, ou MCMC non chargé. + */ + fun compute( + treatments: List, + doseLogs: List, + startMs: Long, + endMs: Long, + stepMs: Long, + nowMs: Long, + nbCurves: Int = DEFAULT_NB_CURVES + ): List> { + if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return emptyList() + + // ── Porteurs : traitements E2 actifs à profil ESE avec ester couvert ── + val carriers = treatments.filter { + it.type == TreatmentType.ESTRADIOL && it.isActive && + it.usesProfileModel && it.pkModel == PKModels.ESTRANNAISE && + EstrannaiseModels.hasModel(it.esterType) + } + if (carriers.isEmpty()) return emptyList() + + val carrierIds = carriers.map { it.id }.toSet() + val carrierDoses = doseLogs + .filter { it.treatmentId in carrierIds && it.timestamp <= endMs } + .ifEmpty { return emptyList() } + + // ── Échantillons échelonnés du posterior (déterministes) ───────────── + // Un échantillon m est appliqué À TOUS les esters du nuage (même + // index) : le nuage explore le posterior ester par ester, index par + // index — reproductible. + val samplesByEster = carriers.associate { + it.esterType to EstrannaiseModels.mcmcSamples(it.esterType) + } + if (samplesByEster.values.any { it.isEmpty() }) return emptyList() + val maxSamples = samplesByEster.values.minOf { it.size } + val stride = (maxSamples.toDouble() / nbCurves.coerceAtMost(maxSamples)).toInt() + .coerceAtLeast(1) + + // Grille : de max(start, 1ʳᵉ dose du porteur) à endMs — M=0 avant la + // 1ʳᵉ dose (rien à faire varier) + val firstDoseMs = carrierDoses.minOf { it.timestamp } + val t0 = maxOf(startMs, firstDoseMs) + if (t0 > endMs) return emptyList() + + // ── M courbes ───────────────────────────────────────────────────────── + val cloud = mutableListOf>() + for (m in 0 until nbCurves) { + val sampleIdx = (m * stride).coerceAtMost(maxSamples - 1) + val paramsByEster = samplesByEster.mapValues { (_, samples) -> + samples[sampleIdx.coerceAtMost(samples.size - 1)] + } + val curve = mutableListOf() + var t = t0 + while (t <= endMs) { + var e2 = 0.0 + for (dose in carrierDoses) { + if (dose.timestamp > t) continue + val dtH = (t - dose.timestamp) / 3_600_000.0 + val tr = carriers.first { it.id == dose.treatmentId } + val params = paramsByEster.getValue(tr.esterType) + e2 += EstrannaiseModels.sampleWithParams(tr.esterType, dtH, params) * dose.doseAmount + } + curve.add(LevelPoint(t, e2, 0.0)) + t += stepMs + } + if (curve.size >= 2) cloud.add(curve) + } + return cloud + } +} diff --git a/app/src/main/java/com/hormonetrack/pk/EstrannaiseModels.kt b/app/src/main/java/com/hormonetrack/pk/EstrannaiseModels.kt new file mode 100644 index 0000000..a04c3e9 --- /dev/null +++ b/app/src/main/java/com/hormonetrack/pk/EstrannaiseModels.kt @@ -0,0 +1,188 @@ +package com.hormonetrack.pk + +import android.content.Context +import com.google.gson.JsonParser +import kotlin.math.exp +import kotlin.math.ln + +/** + * Modèle **Estrannaise analytique** (v1.9.0) — remplace définitivement les + * tables horaires ODS. + * + * SOURCE ORIGINELLE : le dépôt https://github.com/WHSAH/estrannaise.js/ + * (site estrannaise) — le modèle 3 compartiments avec les paramètres + * d'ajustement **publiés par Estrannaise** eux-mêmes : + * + * ``` + * dB(t)/dt = −k1·B(t) + * dEE(t)/dt = k1·B(t) − k2·EE(t) + * dE2(t)/dt = k2·EE(t) − k3·E2(t) + * ``` + * + * forme close pour une dose `dose` (mg) à t = 0 (t en JOURS) : + * + * ``` + * C(t) = dose · d · k1·k2 · [ e^(−k1·t)/((k1−k2)(k1−k3)) + * − e^(−k2·t)/((k1−k2)(k2−k3)) + * + e^(−k3·t)/((k1−k3)(k2−k3)) ] + * ``` + * + * **FIDÉLITÉ (épinglée par EstrannaiseModelsTest)** : cette forme close + * reproduit les anciennes tables ODS **à l'identique** (RMS 0,00 sur 0→200 j + * pour EV/EU/EEn, écart de pic ≤ 0,1 %) — vérifié avant le débranchement : + * l'ODS n'était que l'échantillonnage horaire de ces formules. + * + * **NUAGE D'INCERTITUDE (v1.9.0)** : Estrannaise publie le **posterior + * MCMC** de ses paramètres (313 échantillons `(d, k1, k2, k3)` par ester, + * embarqués dans l'asset `mcmc_samples.json` — cf §15). Le graphique peut + * dessiner un **nuage diffus** de courbes (une par échantillon) qui montre + * la plage d'imprécision possible du modèle — feature exclusive au modèle + * ESE (TFS/WHSAH ne publient pas de posterior). + * + * POURQUOI CE CHANGEMENT (décision v1.9.0) : « tous les modèles ont + * maintenant leurs sources originelles — on abandonne complètement les + * liens avec le fichier ODS, qui lui-même était une extrapolation de ces + * mêmes sources ». Consequences : plus aucune lecture des tables au + * runtime (l'asset `pk_profiles.json` reste uniquement pour les tests de + * fidélité), t½ terminale **analytique** (ln2/min(k1,k2,k3)) au lieu de + * l'estimation par pente de table. + * + * COUVERTURE : 6 esters injectables (EV, EU, EEn, EC, EB, EUCS) — le repo + * source expose aussi 2 patchs (`patch tw`/`patch ow`) et un cycle + * menstruel : hors scope v1.9.0 (documenté §7.12/§19). + * + * PUR sauf [init]/[initWithJson] (lecture de l'asset MCMC) — la formule + * elle-même est testable en JVM. + */ +object EstrannaiseModels { + + /** Paramètres d'un ester : d (facteur), k1/k2/k3 (vitesses, j⁻¹). */ + data class Param(val d: Double, val k1: Double, val k2: Double, val k3: Double) { + /** t½ terminale ANALYTIQUE : la phase la plus lente domine la queue. */ + val terminalHalfLifeDays: Double + get() = ln(2.0) / minOf(k1, k2, k3) + } + + /** + * Paramètres d'ajustement publiés par Estrannaise (src/modeldata.js, + * `PKParameters`). Clés = clés esters de l'app. ⚠️ Mapping depuis les + * clés du repo : « EV im »→EV, « EUn im »→EU, « EEn im »→EEn, + * « EC im »→EC, « EB im »→EB, « EUn casubq »→EUCS (nouveau code ester + * v1.9.0 : undécylate en suspension cristalline). + */ + private val PK_PARAMETERS: Map = mapOf( + "EV" to Param(478.0, 0.236, 4.85, 1.24), + "EU" to Param(471.5, 0.01729, 6.528, 2.285), + "EEN" to Param(191.4, 0.119, 0.601, 0.402), + "EC" to Param(246.0, 0.0825, 3.57, 0.669), + "EB" to Param(1893.1, 0.67, 61.5, 4.34), + "EUCS" to Param(16.15, 0.046, 0.022, 0.101), + ) + + fun hasModel(ester: String): Boolean = PK_PARAMETERS.containsKey(ester.uppercase()) + + /** Les paramètres de l'ester, ou null (lookup insensible à la casse). */ + fun model(ester: String): Param? = PK_PARAMETERS[ester.uppercase()] + + /** + * Réponse normalisée (pg/mL PAR mg) à dtHours après une injection de 1 mg, + * forme close exacte (cf KDoc de l'objet). dtHours ≤ 0 → 0. + * ⚠️ Le passage t heures → t jours est fait ici (le modèle travaille en jours). + */ + fun sample(ester: String, dtHours: Double): Double { + val m = model(ester) ?: return 0.0 + if (dtHours <= 0.0) return 0.0 + return curveDays(dtHours / 24.0, 1.0, m) + } + + /** + * Courbe avec un jeu de paramètres DONNÉ (échantillon MCMC) — utilisé par + * le nuage d'incertitude : chaque courbe du nuage superpose les doses du + * porteur ESE avec UN échantillon du posterior. Normalisé par mg comme + * [sample]. + */ + fun sampleWithParams(ester: String, dtHours: Double, p: Param): Double { + if (dtHours <= 0.0) return 0.0 + return curveDays(dtHours / 24.0, 1.0, p) + } + + /** + * Forme close 3C (t en JOURS) — cas général + les 3 cas dégénérés + * (taux égaux → dénominateurs nuls : solution limite, portée du JS + * source de estrannaise.js). + */ + private fun curveDays(t: Double, dose: Double, m: Param): Double { + if (t < 0) return 0.0 + var ret = 0.0 + val (d, k1, k2, k3) = m + if (dose > 0 && d > 0) { + when { + k1 == k2 && k2 == k3 -> + ret += dose * d * k1 * k1 * t * t * exp(-k1 * t) / 2 + k1 == k2 && k2 != k3 -> + ret += dose * d * k1 * k1 * (exp(-k3 * t) - exp(-k1 * t) * (1 + (k1 - k3) * t)) / + (k1 - k3) / (k1 - k3) + k1 != k2 && k1 == k3 -> + ret += dose * d * k1 * k2 * (exp(-k2 * t) - exp(-k1 * t) * (1 + (k1 - k2) * t)) / + (k1 - k2) / (k1 - k2) + k1 != k2 && k2 == k3 -> + ret += dose * d * k1 * k2 * (exp(-k1 * t) - exp(-k2 * t) * (1 - (k1 - k2) * t)) / + (k1 - k2) / (k1 - k2) + else -> + ret += dose * d * k1 * k2 * ( + exp(-k1 * t) / (k1 - k2) / (k1 - k3) - + exp(-k2 * t) / (k1 - k2) / (k2 - k3) + + exp(-k3 * t) / (k1 - k3) / (k2 - k3) + ) + } + } + return if (ret.isNaN()) 0.0 else ret + } + + // ── Posterior MCMC (nuage d'incertitude) ──────────────────────────────── + + @Volatile + private var mcmc: Map>? = null + + /** + * Charge le posterior MCMC depuis l'asset `mcmc_samples.json` + * (313 échantillons `(d, k1, k2, k3)` par ester injectable — cf §15). + * Appelé au démarrage ([com.hormonetrack.HormoneTrackApp]) — le nuage + * n'en a besoin qu'au premier affichage, mais le chargement est court + * (≈ 48 Ko) et évite un état « chip actif sans données ». + */ + fun init(context: Context) { + if (mcmc != null) return + synchronized(this) { + if (mcmc != null) return + val json = context.assets.open("mcmc_samples.json").bufferedReader().use { it.readText() } + initWithJson(json) + } + } + + /** Point d'entrée testable (JVM) — même pattern que PKProfileStore.initWithJson. */ + fun initWithJson(json: String) { + synchronized(this) { + val root = JsonParser.parseString(json).asJsonObject + val parsed = LinkedHashMap>() + for ((ester, value) in root.entrySet()) { + val arr = value.asJsonArray + val samples = ArrayList(arr.size()) + for (s in arr) { + val e = s.asJsonArray + samples.add(Param(e[0].asDouble, e[1].asDouble, e[2].asDouble, e[3].asDouble)) + } + parsed[ester.uppercase()] = samples + } + mcmc = parsed + } + } + + /** + * Les échantillons MCMC d'un ester (vide si l'asset n'est pas chargé ou + * ester inconnu) — le nuage d'incertitude en tire M jeux de paramètres + * (échelonnés, déterministes : pas d'aléatoire à chaque frame). + */ + fun mcmcSamples(ester: String): List = + mcmc?.get(ester.uppercase()) ?: emptyList() +} diff --git a/app/src/main/java/com/hormonetrack/pk/LabTiming.kt b/app/src/main/java/com/hormonetrack/pk/LabTiming.kt index 0242186..f158bb4 100644 --- a/app/src/main/java/com/hormonetrack/pk/LabTiming.kt +++ b/app/src/main/java/com/hormonetrack/pk/LabTiming.kt @@ -28,10 +28,10 @@ import kotlin.math.ceil * renseignée (`forecastIntervalDays > 0`) — sans Posologie, pas de creux * prévisible → `null` (la page Analyses affiche alors une invite à la * renseigner) ; - * - **t½ terminale** de l'ester : analytique pour TFS/WHSAH - * (`terminalHalfLifeDays`), lue dans la TABLE pour Estrannaise - * ([PKProfileStore.terminalHalfLifeDays] — la pente que l'extrapolation - * de `sample()` utilise déjà) ; + * - **t½ terminale** de l'ester : ANALYTIQUE pour les trois modèles + * (TFS/WHSAH : `terminalHalfLifeDays` des fits ; ESE : idem depuis + * v1.9.0 — le modèle Estrannaise est devenu analytique, cf + * [EstrannaiseModels]) ; * - **début du régime courant** = 1ʳᵉ dose du traitement actif — PROXY * assumé et documenté : l'app ne trace pas l'historique des éditions de * Posologie/dose, la première dose du traitement est le meilleur @@ -123,8 +123,10 @@ object LabTiming { TransfemScienceModels.model(carrier.esterType)?.terminalHalfLifeDays PKModels.WHSAH -> WhsahModels.model(carrier.esterType)?.terminalHalfLifeDays + // v1.9.0 : ESE analytique (forme close estrannaise.js) — la t½ + // n'est plus estimée par pente de table PKModels.ESTRANNAISE -> - PKProfileStore.terminalHalfLifeDays(carrier.esterType, carrier.pkModel) + EstrannaiseModels.model(carrier.esterType)?.terminalHalfLifeDays else -> null } ?: return null diff --git a/app/src/main/java/com/hormonetrack/pk/PKProfileStore.kt b/app/src/main/java/com/hormonetrack/pk/PKProfileStore.kt index 9f40c1d..97c696b 100644 --- a/app/src/main/java/com/hormonetrack/pk/PKProfileStore.kt +++ b/app/src/main/java/com/hormonetrack/pk/PKProfileStore.kt @@ -1,10 +1,22 @@ package com.hormonetrack.pk -import android.content.Context import com.google.gson.JsonParser import com.hormonetrack.data.model.PKModels import kotlin.math.ln +/** + * ⚠️ v1.9.0 : ce lecteur de tables ODS n'est PLUS UTILISÉ AU RUNTIME — + * le modèle Estrannaise est devenu ANALYTIQUE (pk/EstrannaiseModels.kt, + * fidélité aux tables épinglée RMS 0) et tous les dispatchs moteur lisent + * des sources originelles. Il est conservé UNIQUEMENT pour : + * 1. le **test de fidélité** (EstrannaiseModelsTest : forme close vs + * tables, RMS 0 — garde anti-régression du débranchement) ; + * 2. le jeu de données historique (`pk_profiles.json`, déplacé en + * `src/test/assets/` — l'APK ne l'embarque plus, −550 Ko). + * + * L'ancienne [init] (lecture de l'asset au démarrage) a été retirée : + * [initWithJson] suffit pour les tests. + */ object PKProfileStore { @Volatile @@ -12,15 +24,6 @@ object PKProfileStore { private lateinit var profiles: Map - fun init(context: Context) { - if (initialized) return - synchronized(this) { - if (initialized) return - val json = context.assets.open("pk_profiles.json").bufferedReader().use { it.readText() } - initWithJson(json) - } - } - /** Testable entry point: parse the profiles JSON directly (JVM unit tests). */ fun initWithJson(json: String) { synchronized(this) { diff --git a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt index 388612b..387c392 100644 --- a/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt +++ b/app/src/main/java/com/hormonetrack/pk/PharmacokineticEngine.kt @@ -88,16 +88,22 @@ object PharmacokineticEngine { val model = modelOverride ?: treatment.pkModel // v1.4.0 : le modèle Transfem Science utilise les paramètres // V3C (3 compartiments) de la méta-analyse TFS, évalués en - // forme close — les tables ODS restent le fallback (esters - // sans modèle V3C, ou modèle Estrannaise). + // forme close. // v1.4.6 : le modèle WHSAH (fit license-free de Mona) est la // 3ᵉ voie de dispatch, au même niveau que TFS. + // v1.9.0 : l'Estrannaise devient ANALYTIQUE (forme close de + // estrannaise.js — fidélité aux tables ODS épinglée RMS 0) : + // les 4 voies de dispatch sont toutes des sources originelles, + // PLUS AUCUNE lecture des tables ODS au runtime (décision + // v1.9.0 : l'ODS n'était que l'échantillonnage de ces sources). return when { model == PKModels.TRANSFEM_SCIENCE && TransfemScienceModels.hasModel(ester) -> TransfemScienceModels.sample(ester, dtH) * mg model == PKModels.WHSAH && WhsahModels.hasModel(ester) -> WhsahModels.sample(ester, dtH) * mg - else -> PKProfileStore.sample(ester, model, dtH) * mg + model == PKModels.ESTRANNAISE && EstrannaiseModels.hasModel(ester) -> + EstrannaiseModels.sample(ester, dtH) * mg + else -> 0.0 // ester sans modèle (documenté : ni TFS ni WHSAH ni ESE) } } } @@ -119,7 +125,9 @@ object PharmacokineticEngine { * 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) ; + * - profil Estrannaise → 10 demi-vies TERMINALES analytiques (v1.9.0 ; + * l'ancienne coupure « longueur de table ODS 8001 h » a disparu avec + * les tables du runtime) ; * - Bateman (gel/patch/oral) → 30 demi-vies d'élimination. * * Public (v1.6.0) : réutilisé par [LabTrajectoryModel.extensionHorizonEndMs] @@ -139,8 +147,12 @@ object PharmacokineticEngine { model == PKModels.TRANSFEM_SCIENCE && TransfemScienceModels.hasModel(treatment.esterType) -> TransfemScienceModels.model(treatment.esterType)!! .terminalHalfLifeDays * 24.0 * 10.0 - else -> - PKProfileStore.profileLength(treatment.esterType, model).toDouble() + // v1.9.0 : ESE analytique → t½ terminale ANALYTIQUE (comme + // TFS/WHS) au lieu de la longueur de table ODS + model == PKModels.ESTRANNAISE && EstrannaiseModels.hasModel(treatment.esterType) -> + EstrannaiseModels.model(treatment.esterType)!! + .terminalHalfLifeDays * 24.0 * 10.0 + else -> 0.0 } } else 0.0 val batemanH = if (treatment.usesProfileModel) 0.0 @@ -628,7 +640,13 @@ object PharmacokineticEngine { val median = if (values.size % 2 == 1) values[values.size / 2] else (values[values.size / 2 - 1] + values[values.size / 2]) / 2.0 val factor = Math.pow(10.0, decimals.toDouble()) - return (median * factor).toInt() / factor + // ⚠️ ARRONDI (v1.8.2), pas troncature : l'ancien motif + // `(median * factor).toInt() / factor` tronquait au dernier bit + // flottant — 0.8999999999999999 × 100 → toInt() = 89 → « 0,89 » + // (le modèle ESE analytique a déplacé ce bit, révélant le piège ; + // le bug #61 historique du même genre : 6,5 → 6 avec + // kotlin.math.round — là, Math.round est l'arrondi au plus proche). + return Math.round(median * factor) / factor } fun labIsSignificant(predicted: Double, maxSoFar: Double): Boolean = diff --git a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt index 29a922a..8491ae9 100644 --- a/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt +++ b/app/src/main/java/com/hormonetrack/ui/components/CurveChart.kt @@ -46,7 +46,15 @@ data class ChartSeries( * ses extrema reflèteraient l'interp ρ entre labs, pas la physiologie du * modèle, et `labIsSignificant` a déjà filtré les points aberrants. */ - val showExtrema: Boolean = true + val showExtrema: Boolean = true, + /** + * v1.9.0 : NUAGE D'INCERTITUDE du modèle ESE (échantillons MCMC — cf + * pk/EstrannaiseCloud) : chaque sous-liste est une polyligne dessinée en + * alpha faible SOUS la courbe principale de la série. null = pas de nuage. + * Hors labels/extrema par construction (c'est de l'incertitude, pas des + * courbes nommées). + */ + val cloud: List>? = null ) data class ChartOptions( @@ -402,6 +410,34 @@ fun CurveChart( } } + // NUAGE D'INCERTITUDE (v1.9.0, exclusif ESE) : dessiné EN PREMIER + // (sous toutes les courbes nommées), chaque sous-courbe du posterior + // MCMC en alpha faible. Décisions d'affichage : + // - le nuage ne participe PAS aux échelles (allPoints ne l'inclut + // pas) : c'est une plage d'imprécision, pas des données à cadrer — + // il peut sortir du cadre en haut/bas sans casser la lecture ; + // - ni aux labels ni aux extrema (pas des courbes nommées). + val cloudPaths = mutableListOf>() + for (s in series) { + val cloud = s.cloud ?: continue + for (sub in cloud) { + if (sub.size < 2) continue + val path = Path() + sub.forEachIndexed { i, p -> + val x = xOf(p.timestamp) + val y = yE2(p.e2) + if (i == 0) path.moveTo(x, y) else path.lineTo(x, y) + } + cloudPaths.add(s.e2Style.color.copy(alpha = 0.15f) to path) + } + } + for ((color, path) in cloudPaths) { + drawPath( + path, color, + style = Stroke(width = 1.5.dp.toPx()) + ) + } + // Une courbe E2 + une courbe T par série for (s in series) { if (s.points.size < 2) continue diff --git a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt index 4b65ee0..ba67184 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/ChartScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.hormonetrack.R import com.hormonetrack.pk.LevelPoint +import com.hormonetrack.pk.EstrannaiseCloud import com.hormonetrack.pk.LabTrajectoryModel import com.hormonetrack.pk.PharmacokineticEngine import com.hormonetrack.pk.TConfig @@ -125,6 +126,13 @@ fun ChartScreen() { // revendique « passer par tes labs » et doit rester une référence de // comparaison, pas un affichage par défiant var showLabTrack by remember { mutableStateOf(false) } + // v1.9.0 : NUAGE D'INCERTITUDE du modèle ESE (échantillons MCMC de + // estrannaise.js — cf pk/EstrannaiseCloud) — OFF par défaut, opt-in, + // activable/désactivable à volonté ; ACTIVABLE SEULEMENT quand le + // modèle ESE est affiché (le nuage est exclusif à ce modèle — les + // fits TFS/WHSAH ne publient pas de posterior). Se désactive + // automatiquement si ESE est éteint. + var showCloud 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 @@ -270,19 +278,30 @@ fun ChartScreen() { } } + /** + * Une courbe rendue par le producer : clé de modèle ("ESE"/"TFS"/"WHS"/ + * "LAB"/"LABX"), ses points, et — v1.9.0 — son NUAGE D'INCERTITUDE + * (uniquement pour "ESE" quand le chip Nuage est actif). + */ + data class ChartCurve( + val model: String, + val points: List, + val cloud: List> = emptyList() + ) + // 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.6.0 : la 5ᵉ est "LABX" (partie PROLONGÉE du tracé labs, ρ constant) val curves by produceState( - emptyList>>(), + emptyList(), autoByModel, doseLogs, startMs, endMs, showEse, showTfs, showWhs, showForecast, - showLabTrack, showLabTrackExtend, + showLabTrack, showLabTrackExtend, showCloud, // Clé = la liste ENTIÈRE des labs (pas .size, fix #66) : éditer la // VALEUR d'une analyse doit recalculer les courbes labResults ) { withContext(Dispatchers.Default) { - val list = mutableListOf>>() + val list = mutableListOf() // ── v1.5.0 : courbe « Tracé labs » ─────────────────────────────── // Totalement indépendante des toggles de modèles (elle utilise le // modèle STOCKÉ des traitements — sémantique Home, cf doc §7.10) @@ -309,10 +328,10 @@ fun ChartScreen() { 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) + list.add(ChartCurve("LAB", anchored)) + if (extended.isNotEmpty()) list.add(ChartCurve("LABX", extended)) } else { - list.add("LAB" to labCurve.points) + list.add(ChartCurve("LAB", labCurve.points)) } } if (showEse || showTfs || showWhs) { @@ -336,22 +355,29 @@ fun ChartScreen() { for ((model, enabled) in listOf("ESE" to showEse, "TFS" to showTfs, "WHS" to showWhs)) { if (!enabled) continue val autoM = autoByModel[model] - list.add( - model to PharmacokineticEngine.computeCurve( - effTreatments, allDoses, startMs, endMs, - stepMs = step, - tConfig = autoM?.tConfig ?: effTConfig, modelOverride = model, - scalePerEster = autoM?.esterScales, - tKPerEster = autoM?.tKPerEster - ) + val curve = PharmacokineticEngine.computeCurve( + effTreatments, allDoses, startMs, endMs, + stepMs = step, + tConfig = autoM?.tConfig ?: effTConfig, modelOverride = model, + scalePerEster = autoM?.esterScales, + tKPerEster = autoM?.tKPerEster ) + // v1.9.0 : le nuage d'incertitude est attaché à la série + // ESE (exclusif à ce modèle — les fits TFS/WHSAH ne + // publient pas de posterior MCMC) + val cloud = if (model == "ESE" && showCloud) { + EstrannaiseCloud.compute( + treatments, doseLogs, startMs, endMs, step, nowMs + ) + } else emptyList() + list.add(ChartCurve(model, curve, cloud)) } } value = list } } - val allPoints = curves.flatMap { it.second } + val allPoints = curves.flatMap { it.points } val (a, b) = if (allPoints.size >= 2) { allPoints.minOf { it.timestamp } to allPoints.maxOf { it.timestamp } } else 0L to 0L @@ -363,9 +389,9 @@ fun ChartScreen() { // Série visuelle : E2 Estrannaise = bleu plein, E2 TFS = turquoise plein ; // T Estrannaise = rose plein, T TFS = rose pointillé. - val series = curves.map { (model, points) -> + val series = curves.map { (model, points, cloud) -> when (model) { - "ESE" -> ChartSeries(points, CurveStyle(ChartE2), CurveStyle(ChartT)) + "ESE" -> ChartSeries(points, CurveStyle(ChartE2), CurveStyle(ChartT), cloud = curves.first { it.model == "ESE" }.cloud) // v1.4.6 : WHSAH = E2 vert plein, T violet pointillé "WHS" -> ChartSeries(points, CurveStyle(ChartWhs), CurveStyle(ChartTWhs, dashed = true)) // v1.5.0 : « Tracé labs » = E2 rose foncé pointillé, PAS de courbe T @@ -490,7 +516,12 @@ fun ChartScreen() { ) { FilterChip( selected = showEse, - onClick = { showEse = !showEse }, + onClick = { + showEse = !showEse + // v1.9.0 : le nuage est exclusif à ESE — éteindre ESE + // éteint le nuage (sinon il flotterait sans sa courbe) + if (!showEse) showCloud = false + }, label = { Text(stringResource(R.string.model_ese)) } ) FilterChip( @@ -519,6 +550,16 @@ fun ChartScreen() { enabled = showLabTrack, label = { Text(stringResource(R.string.show_lab_track_extend)) } ) + // v1.9.0 : NUAGE D'INCERTITUDE (exclusif ESE) — off par défaut, + // activable à volonté mais SEULEMENT si ESE est affiché (même + // pattern que Prolonger/Tracé labs) ; éteindre ESE coupe le + // nuage automatiquement (sinon le nuage flotterait sans sa courbe) + FilterChip( + selected = showCloud, + onClick = { showCloud = !showCloud }, + enabled = showEse, + label = { Text(stringResource(R.string.show_cloud)) } + ) } Spacer(Modifier.height(12.dp)) @@ -688,18 +729,27 @@ fun ChartScreen() { } // v1.5.0 : légende « Tracé labs » — seulement si la série // a ≥ 2 points (≥ 2 labs significatifs, la grille est vide sinon) - if (showLabTrack && curves.any { it.first == "LAB" && it.second.size >= 2 }) { + if (showLabTrack && curves.any { it.model == "LAB" && it.points.size >= 2 }) { Text( stringResource(R.string.legend_lab_track), style = MaterialTheme.typography.labelMedium, color = ChartLabTrajectory ) } + // v1.9.0 : légende du nuage d'incertitude (ESE) — visible + // tant que le nuage est affiché (série ESE avec cloud non vide) + if (showCloud && showEse && curves.any { it.model == "ESE" && it.cloud.isNotEmpty() }) { + Text( + stringResource(R.string.legend_cloud), + style = MaterialTheme.typography.labelMedium, + color = ChartE2.copy(alpha = 0.55f) + ) + } // 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 } + curves.any { it.model == "LABX" && it.points.size >= 2 } if (labExtendVisible) { Text( stringResource(R.string.legend_lab_track_extend), diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d17683a..cc4e61b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -44,6 +44,10 @@ Prolonger + + Nuage + Nuage pointillé = incertitude des paramètres MCMC de l\'Estrannaise (modèle ESE seul) Zoom avant (fenêtre plus courte) Zoom arrière (fenêtre plus longue) Sous THS depuis le %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ec16bc..0bc8454 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,6 +44,10 @@ Extend + + Cloud + Dotted cloud = Estrannaise MCMC parameter uncertainty (ESE model only) Zoom in (shorter window) Zoom out (longer window) Under HRT since %1$s diff --git a/app/src/main/assets/pk_profiles.json b/app/src/test/assets/pk_profiles.json similarity index 100% rename from app/src/main/assets/pk_profiles.json rename to app/src/test/assets/pk_profiles.json diff --git a/app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt b/app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt index 68f571a..dc23a74 100644 --- a/app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/CalibrationPerModelTest.kt @@ -48,8 +48,9 @@ class CalibrationPerModelTest { // (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") + val file = java.io.File("src/test/assets/pk_profiles.json") .takeIf { it.exists() } + ?: 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()) } diff --git a/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt b/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt new file mode 100644 index 0000000..883c3c5 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/pk/EstrannaiseCloudTest.kt @@ -0,0 +1,109 @@ +package com.hormonetrack.pk + +import com.hormonetrack.data.model.AdministrationRoute +import com.hormonetrack.data.model.DoseLog +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.Before +import org.junit.Test +import java.io.File + +/** + * Tests du NUAGE D'INCERTITUDE (v1.9.0 — cf doc §7.12) : + * [EstrannaiseCloud.compute] — feature exclusive au modèle ESE (les fits + * TFS/WHSAH ne publient pas de posterior MCMC). + * + * Épinglés : + * - 32 courbes par défaut, chacune ≥ 2 points, dans la fenêtre demandée ; + * - DISPERSION réelle : les courbes du nuage DIFFÈRENT entre elles (les + * échantillons MCMC couvrent une plage de paramètres — un nuage où toutes + * les courbes coïncideraient serait un bug de chargement) ; + * - non calculable → vide : pas de traitement ESE porteur (TFS/oral), + * MCMC non chargé, nbCurves < 2. + */ +class EstrannaiseCloudTest { + + private val DAY_MS = 86_400_000L + private val NOW = 1_790_000_000_000L + + /** Traitement EEn ESE actif, 5 mg hebdo sur 4 semaines. */ + private fun ese() = Treatment( + id = 1, name = "EEn ESE", type = TreatmentType.ESTRADIOL, + route = AdministrationRoute.INJECTION_IM, + doseAmount = 5.0, doseUnit = "mg", esterType = "EEN", pkModel = "ESE", + forecastIntervalDays = 7.0, createdAt = 0 + ) + + private fun doses() = (4 downTo 1).map { k -> + DoseLog(id = k.toLong(), treatmentId = 1, timestamp = NOW - k * 7 * DAY_MS, doseAmount = 5.0) + } + + @Before + fun setup() { + if (EstrannaiseModels.mcmcSamples("EEN").isEmpty()) { + val candidates = listOf( + File("src/main/assets/mcmc_samples.json"), + File("app/src/main/assets/mcmc_samples.json") + ) + val file = candidates.firstOrNull { it.exists() } + ?: error("mcmc_samples.json introuvable (nuage d'incertitude)") + EstrannaiseModels.initWithJson(file.readText()) + } + } + + @Test + fun `cloud has 32 curves with real dispersion between them`() { + val cloud = EstrannaiseCloud.compute( + listOf(ese()), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW + ) + assertEquals("32 courbes par défaut", 32, cloud.size) + assertTrue(cloud.all { it.size >= 2 }) + // DISPERSION : le max E2 de chaque courbe diffère d'une courbe à + // l'autre (sinon tous les échantillons seraient identiques) + val peaks = cloud.map { c -> c.maxOf { it.e2 } }.distinct() + assertTrue( + "les courbes du nuage divergent (${peaks.size} maxima distincts)", + peaks.size >= 8 + ) + } + + @Test + fun `cloud stays within the requested window`() { + val start = NOW - 10 * DAY_MS + val end = NOW + val cloud = EstrannaiseCloud.compute( + listOf(ese()), doses(), start, end, 3_600_000L, NOW + ) + assertTrue(cloud.all { c -> c.all { it.timestamp in start..end } }) + } + + @Test + fun `non-ESE carriers produce no cloud (feature exclusive to ESE)`() { + // TFS : pas de posterior publié → nuage vide + val tfs = ese().copy(pkModel = "TFS") + assertTrue(EstrannaiseCloud.compute(listOf(tfs), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW).isEmpty()) + // Oral (Bateman) : idem + val oral = Treatment( + id = 2, name = "CPA", type = TreatmentType.ANTI_ANDROGEN, + route = AdministrationRoute.ORAL, doseAmount = 12.5, doseUnit = "mg", + esterType = "NONE", forecastIntervalDays = 2.0, createdAt = 0 + ) + assertTrue(EstrannaiseCloud.compute(listOf(oral), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW).isEmpty()) + } + + @Test + fun `no doses on the carrier produces no cloud`() { + assertTrue(EstrannaiseCloud.compute( + listOf(ese()), emptyList(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW + ).isEmpty()) + } + + @Test + fun `nbCurves below 2 produces no cloud`() { + assertTrue(EstrannaiseCloud.compute( + listOf(ese()), doses(), NOW - 30 * DAY_MS, NOW, 3_600_000L, NOW, nbCurves = 1 + ).isEmpty()) + } +} diff --git a/app/src/test/java/com/hormonetrack/pk/EstrannaiseModelsTest.kt b/app/src/test/java/com/hormonetrack/pk/EstrannaiseModelsTest.kt new file mode 100644 index 0000000..74e8894 --- /dev/null +++ b/app/src/test/java/com/hormonetrack/pk/EstrannaiseModelsTest.kt @@ -0,0 +1,161 @@ +package com.hormonetrack.pk + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.File + +/** + * Tests du modèle **Estrannaise analytique** (v1.9.0 — cf doc §7.12). + * + * CONTEXTE : depuis v1.9.0, le modèle ESE n'est plus les tables horaires ODS + * mais la **forme close 3C** publiée par estrannaise.js (les mêmes sources + * dont l'ODS n'était que l'échantillonnage horaire). Chaque test épingle : + * - **FIDÉLITÉ** : la forme close reproduit les tables ODS à l'identique + * (garde anti-régression du débranchement — RMS ≈ 0) ; + * - les **cas dégénérés** de taux égaux (solutions limites portées du JS + * source — jamais NaN) ; + * - le **posterior MCMC** chargé (313 échantillons par ester) et sa t½ + * analytique. + */ +class EstrannaiseModelsTest { + + private val DAY_MS = 86_400_000L + + @Before + fun setup() { + // Tables ODS (src/test/assets depuis v1.9.0) : référence de fidélité + if (!PKProfileStore.hasProfile("EV", "ESE")) { + val candidates = listOf( + File("src/test/assets/pk_profiles.json"), + File("src/main/assets/pk_profiles.json"), + File("app/src/test/assets/pk_profiles.json") + ) + val file = candidates.firstOrNull { it.exists() } + ?: error("pk_profiles.json introuvable pour les tests de fidélité") + PKProfileStore.initWithJson(file.readText()) + } + // Posterior MCMC (asset runtime v1.9.0) + if (EstrannaiseModels.mcmcSamples("EV").isEmpty()) { + val candidates = listOf( + File("src/main/assets/mcmc_samples.json"), + File("app/src/main/assets/mcmc_samples.json") + ) + val file = candidates.firstOrNull { it.exists() } + ?: error("mcmc_samples.json introuvable (nuage d'incertitude)") + EstrannaiseModels.initWithJson(file.readText()) + } + } + + /** FIDÉLITÉ (le cœur du débranchement v1.9.0) : forme close == tables ODS. */ + @Test + fun `analytic shape reproduces the ODS tables exactly`() { + val params = mapOf( + "EV" to EstrannaiseModels.model("EV")!!, + "EU" to EstrannaiseModels.model("EU")!!, + "EEN" to EstrannaiseModels.model("EEN")!! + ) + for ((ester, m) in params) { + val profiles = com.google.gson.JsonParser.parseString( + File("src/test/assets/pk_profiles.json").let { + if (!it.exists()) File("app/src/test/assets/pk_profiles.json").readText() else it.readText() + } + ).asJsonObject.getAsJsonObject("profiles") + val key = profiles.keySet().firstOrNull { + it.lowercase().startsWith(ester.lowercase() + "_") && it.lowercase().endsWith("ese") + } ?: error("table $ester introuvable") + val arr = profiles.getAsJsonArray(key) + + var maxAbs = 0.0 + var sumSq = 0.0 + for (h in 0..4800) { // 0 → 200 j + val analytic = EstrannaiseModels.sample(ester, h.toDouble()) + val table = arr[h].asDouble + val diff = analytic - table + maxAbs = maxOf(maxAbs, kotlin.math.abs(diff)) + sumSq += diff * diff + } + val rms = kotlin.math.sqrt(sumSq / 4801) + assertTrue( + "$ester : forme close == table (maxAbs=$maxAbs, rms=$rms)", + maxAbs <= 0.01 && rms <= 0.01 + ) + } + } + + /** Les pics publiés (tables ODS) sont reproduits par la forme close. */ + @Test + fun `published peaks match the analytic curve`() { + // (pic table @ arrondi h ; scan analytique au pas de 6 min) + data class Expect(val ester: String, val peak: Double, val tPeakHours: Int) + for (e in listOf( + Expect("EV", 61.12, 45), Expect("EU", 3.44, 55), Expect("EEN", 31.35, 152) + )) { + val m = EstrannaiseModels.model(e.ester)!! + var peak = 0.0 + var tPeak = 0.0 + var t = 0.0 + while (t <= 200.0) { + val v = EstrannaiseModels.sample(e.ester, t * 24.0) + if (v > peak) { peak = v; tPeak = t } + t += 0.25 / 24.0 + } + assertEquals("pic ${e.ester}", e.peak, peak, e.peak * 0.002) + // ±3,5 h : le sommet de la table (échantillonnée à l'heure, + // arrondie) est décalé vs le pic continu (EEN : 152 → 154,5 h) + assertEquals("Tmax ${e.ester}", e.tPeakHours.toDouble(), tPeak * 24.0, 3.5) + assertNotNull(m) + } + } + + /** Taux dégénérés : les solutions limites ne produisent ni NaN ni négatif. */ + @Test + fun `degenerate equal rates use the limit solutions (no NaN)`() { + for (p in listOf( + EstrannaiseModels.Param(478.0, 0.236, 0.236, 1.24), // k1 == k2 + EstrannaiseModels.Param(478.0, 0.236, 1.24, 0.236), // k1 == k3 + EstrannaiseModels.Param(478.0, 0.236, 1.24, 1.24), // k2 == k3 + EstrannaiseModels.Param(478.0, 0.5, 0.5, 0.5) // tous égaux + )) { + for (t in listOf(0.5, 1.0, 24.0, 240.0, 2400.0)) { // heures + val v = EstrannaiseModels.sampleWithParams("EV", t, p) + assertTrue("finie et ≥ 0 (t=$t, k=(${p.k1},${p.k2},${p.k3}))", v >= 0.0 && !v.isNaN()) + } + } + } + + /** Posterior MCMC : chargé, 313 échantillons par ester, couverture réelle. */ + @Test + fun `mcmc posterior is loaded with 313 samples per injectable ester`() { + for (ester in listOf("EV", "EU", "EEN", "EC", "EB", "EUCS")) { + val samples = EstrannaiseModels.mcmcSamples(ester) + assertEquals("313 échantillons ($ester)", 313, samples.size) + // les échantillons couvrent une PLAGE (premier ≠ dernier) + assertTrue( + "le posterior $ester couvre une plage", + samples.first().d != samples.last().d + ) + } + } + + /** t½ analytique : EV = ln2/min(0,236 ; 4,85 ; 1,24) ≈ 2,94 j. */ + @Test + fun `terminal half-life is analytic (ln2 over slowest rate)`() { + val tHalf = EstrannaiseModels.model("EV")!!.terminalHalfLifeDays + assertEquals(kotlin.math.ln(2.0) / 0.236, tHalf, 1e-12) + } + + /** Normalisation par mg + gardes (dt ≤ 0, ester inconnu). */ + @Test + fun `sample is normalized per mg with guards`() { + val one = EstrannaiseModels.sample("EEN", 24.0) // 1 mg + val five = 5.0 * one + assertEquals(5.0 * one, five, 1e-12) // linéarité (trivial mais épinglé) + assertEquals(0.0, EstrannaiseModels.sample("EEN", 0.0), 1e-12) + assertEquals(0.0, EstrannaiseModels.sample("EEN", -1.0), 1e-12) + assertEquals(0.0, EstrannaiseModels.sample("XXX", 24.0), 1e-12) + assertTrue(EstrannaiseModels.sample("EUCS", 24.0) > 0.0) // EUCS couvert + } +} diff --git a/app/src/test/java/com/hormonetrack/pk/ExtremaTest.kt b/app/src/test/java/com/hormonetrack/pk/ExtremaTest.kt index adde80b..49fa86e 100644 --- a/app/src/test/java/com/hormonetrack/pk/ExtremaTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/ExtremaTest.kt @@ -27,6 +27,7 @@ class ExtremaTest { fun setup() { if (!PKProfileStore.hasProfile("EV", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/LabTimingTest.kt b/app/src/test/java/com/hormonetrack/pk/LabTimingTest.kt index e3cc68e..8ee6f81 100644 --- a/app/src/test/java/com/hormonetrack/pk/LabTimingTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/LabTimingTest.kt @@ -38,6 +38,7 @@ class LabTimingTest { fun setup() { if (!PKProfileStore.hasProfile("EV", "ESE")) { val candidates = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ) @@ -199,17 +200,18 @@ class LabTimingTest { } @Test - fun `Estrannaise carrier reads its half-life from the ODS table`() { - // EV / tables ODS : t½ estimée depuis la pente terminale de la table — - // finie et raisonnable, et la recommandation se calcule sans crash + fun `Estrannaise carrier uses its ANALYTIC half-life (v1_9_0)`() { + // v1.9.0 : le modèle ESE est devenu analytique (forme close de + // estrannaise.js) — la t½ = ln2/min(k1,k2,k3) des PKParameters, plus + // d'estimation par pente de table. Finie, raisonnable, et la + // recommandation se calcule sans crash. val evEse = eenTfs().copy(esterType = "EV", pkModel = "ESE", forecastIntervalDays = 14.0) val doses = listOf( DoseLog(id = 1, treatmentId = 1, timestamp = NOW - 70 * DAY_MS, doseAmount = 4.0), DoseLog(id = 2, treatmentId = 1, timestamp = NOW - 56 * DAY_MS, doseAmount = 4.0) ) - val tHalf = PKProfileStore.terminalHalfLifeDays("EV", "ESE") - assertNotNull(tHalf) - assertTrue("t½ EV ODS plausible ($tHalf j)", tHalf!! in 0.1..200.0) + val tHalf = EstrannaiseModels.model("EV")!!.terminalHalfLifeDays + assertTrue("t½ EV analytique plausible ($tHalf j)", tHalf in 0.1..200.0) val rec = LabTiming.nextBloodDrawRecommendation(listOf(evEse), doses, emptyList(), NOW) assertNotNull(rec) assertEquals(tHalf, rec!!.terminalHalfLifeDays, 1e-9) diff --git a/app/src/test/java/com/hormonetrack/pk/LabTrajectoryModelTest.kt b/app/src/test/java/com/hormonetrack/pk/LabTrajectoryModelTest.kt index 90b98a4..5ddef72 100644 --- a/app/src/test/java/com/hormonetrack/pk/LabTrajectoryModelTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/LabTrajectoryModelTest.kt @@ -40,6 +40,7 @@ class LabTrajectoryModelTest { // module (les assets Android ne sont pas sur le classpath des tests JVM) if (!PKProfileStore.hasProfile("EV", "ESE")) { val candidates = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ) diff --git a/app/src/test/java/com/hormonetrack/pk/PKProfileStoreTest.kt b/app/src/test/java/com/hormonetrack/pk/PKProfileStoreTest.kt index b3f0906..3179dcd 100644 --- a/app/src/test/java/com/hormonetrack/pk/PKProfileStoreTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/PKProfileStoreTest.kt @@ -19,6 +19,7 @@ class PKProfileStoreTest { fun setup() { if (!PKProfileStore.hasProfile("EV", "ESE")) { val candidates = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ) diff --git a/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt b/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt index 45739e3..8331bb5 100644 --- a/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/PharmacokineticEngineTest.kt @@ -25,6 +25,7 @@ class PharmacokineticEngineTest { fun setup() { if (!PKProfileStore.hasProfile("EV", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } @@ -159,7 +160,16 @@ class PharmacokineticEngineTest { LabResult(marker = "E2", value = predicted * r, unit = "pg/mL", timestamp = t) } val sf = PharmacokineticEngine.computeScaleFactor(tr, doses, labs) - assertEquals(0.9, sf!!, 0.01) + // DIAG v1.8.2 : comparaison e2At (chemin production) vs Σ + // concentrationOfDose (chemin computeScaleFactor) + val ratiosDiag = labs.sortedBy { it.timestamp }.map { lab -> + val viaE2At = PharmacokineticEngine.e2At(listOf(tr), doses, lab.timestamp) + val viaDirect = doses.filter { it.timestamp <= lab.timestamp }.sumOf { d -> + PharmacokineticEngine.concentrationOfDose(tr, d, lab.timestamp) + } + "e2At=%.6f/direct=%.6f/ratioSF=%.6f".format(viaE2At, viaDirect, lab.value / viaDirect) + } + assertEquals("ratios=$ratiosDiag sf=$sf", 0.9, sf!!, 0.01) } @Test diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase2Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase2Test.kt index 2ee5f91..afa67b2 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase2Test.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase2Test.kt @@ -43,6 +43,7 @@ class RegressionUserCase2Test { ) if (!PKProfileStore.hasProfile("EEN", "TFS")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt index c812d4f..e28bad9 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase3Test.kt @@ -42,6 +42,7 @@ class RegressionUserCase3Test { ) if (!PKProfileStore.hasProfile("EV", "TFS")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt index acb17cc..60c368d 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase4Test.kt @@ -50,6 +50,7 @@ class RegressionUserCase4Test { ) if (!PKProfileStore.hasProfile("EV", "TFS")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt index 9457d09..86b370f 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCase5Test.kt @@ -52,6 +52,7 @@ class RegressionUserCase5Test { ) if (!PKProfileStore.hasProfile("EV", "TFS")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/RegressionUserCaseTest.kt b/app/src/test/java/com/hormonetrack/pk/RegressionUserCaseTest.kt index 74af5a2..bf0d6ef 100644 --- a/app/src/test/java/com/hormonetrack/pk/RegressionUserCaseTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/RegressionUserCaseTest.kt @@ -40,6 +40,7 @@ class RegressionUserCaseTest { ) if (!PKProfileStore.hasProfile("EEN", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/ScaleFactorWhsahReproTest.kt b/app/src/test/java/com/hormonetrack/pk/ScaleFactorWhsahReproTest.kt index 1eda3e8..06e4efb 100644 --- a/app/src/test/java/com/hormonetrack/pk/ScaleFactorWhsahReproTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/ScaleFactorWhsahReproTest.kt @@ -49,8 +49,9 @@ class ScaleFactorWhsahReproTest { fun setup() { assumeTrue("export v1.4.2 absent (données locales)", DATA_FILE.exists()) if (!PKProfileStore.hasProfile("EEN", "ESE")) { - val file = File("src/main/assets/pk_profiles.json") + val file = File("src/test/assets/pk_profiles.json") .takeIf { it.exists() } + ?: File("src/main/assets/pk_profiles.json").takeIf { it.exists() } ?: File("app/src/main/assets/pk_profiles.json") PKProfileStore.initWithJson(file.readText()) } diff --git a/app/src/test/java/com/hormonetrack/pk/TransfemScienceEngineTest.kt b/app/src/test/java/com/hormonetrack/pk/TransfemScienceEngineTest.kt index e9784c6..e4f97a2 100644 --- a/app/src/test/java/com/hormonetrack/pk/TransfemScienceEngineTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/TransfemScienceEngineTest.kt @@ -47,6 +47,7 @@ class TransfemScienceEngineTest { // l'asset pour rester au plus près du comportement réel de l'app. if (!PKProfileStore.hasProfile("EV", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } @@ -79,14 +80,24 @@ class TransfemScienceEngineTest { } @Test - fun `EB forced to ESE model yields zero (no ODS table for this ester)`() { + fun `EB forced to ESE model yields a REAL curve (analytic since v1_9_0)`() { + // v1.9.0 : l'Estrannaise est devenue ANALYTIQUE (estrannaise.js) et + // couvre désormais EB (fit « EB im ») — l'ancien test épinglait + // « EB+ESE → 0, pas de table ODS pour cet ester » (obsolète) val tr = treatment(Esters.EB, doseMg = 5.0) val dose = DoseLog(treatmentId = 1L, timestamp = BASE, doseAmount = 5.0) val curve = PharmacokineticEngine.computeCurve( listOf(tr), listOf(dose), BASE, BASE + 5 * DAY_MS, tConfig = TConfig(), modelOverride = PKModels.ESTRANNAISE ) - assertTrue(curve.all { it.e2 == 0.0 }) + assertTrue("EB ESE analytique produit une courbe non vide", curve.any { it.e2 > 0.0 }) + // et un ester réellement NON couvert par les trois modèles → 0 + val unknown = tr.copy(esterType = "XXX") + val curve2 = PharmacokineticEngine.computeCurve( + listOf(unknown), listOf(dose), BASE, BASE + 5 * DAY_MS, + tConfig = TConfig(), modelOverride = PKModels.ESTRANNAISE + ) + assertTrue("ester inconnu → 0 dans les trois modèles", curve2.all { it.e2 == 0.0 }) } @Test diff --git a/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt b/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt index 3d25c23..5b37420 100644 --- a/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/V120FeaturesTest.kt @@ -30,6 +30,7 @@ class V120FeaturesTest { fun setup() { if (!PKProfileStore.hasProfile("EEN", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } @@ -263,7 +264,9 @@ class V120FeaturesTest { @Test fun `scalePerEster scales each dose by its own ester factor`() { val tr = eenTreatment().copy(esterType = Esters.EV, scaleFactor = 999.0) // ne doit PAS être utilisé - val evDose = DoseLog(treatmentId = 1, timestamp = BASE - 30 * DAY, doseAmount = 4.0, esterType = Esters.EV) + // v1.9.0 : la dose EV doit être DANS le cutoff ESE analytique + // (10 × t½ EV = 29,4 j — l'ancien cutoff ODS était 333 j) : −10 j + val evDose = DoseLog(treatmentId = 1, timestamp = BASE - 10 * DAY, doseAmount = 4.0, esterType = Esters.EV) val eenDose = DoseLog(treatmentId = 1, timestamp = BASE - 3 * DAY, doseAmount = 5.0, esterType = Esters.EEN) val scales = mapOf("EV" to 0.5, "EEN" to 2.0) val t = BASE diff --git a/app/src/test/java/com/hormonetrack/pk/WhsahEngineTest.kt b/app/src/test/java/com/hormonetrack/pk/WhsahEngineTest.kt index 26ffe57..f64bf16 100644 --- a/app/src/test/java/com/hormonetrack/pk/WhsahEngineTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/WhsahEngineTest.kt @@ -41,6 +41,7 @@ class WhsahEngineTest { // les tables ODS servent au chemin ESE (test de contraste) if (!PKProfileStore.hasProfile("EV", "ESE")) { val file = listOf( + File("src/test/assets/pk_profiles.json"), File("src/main/assets/pk_profiles.json"), File("app/src/main/assets/pk_profiles.json") ).first { it.exists() } diff --git a/app/src/test/java/com/hormonetrack/pk/WhsahModelsTest.kt b/app/src/test/java/com/hormonetrack/pk/WhsahModelsTest.kt index 75410b5..a984969 100644 --- a/app/src/test/java/com/hormonetrack/pk/WhsahModelsTest.kt +++ b/app/src/test/java/com/hormonetrack/pk/WhsahModelsTest.kt @@ -90,7 +90,10 @@ class WhsahModelsTest { assertEquals(listOf("EV", "EU", "EEN", "EB", "EC", "ECS"), com.hormonetrack.data.model.Esters.choicesForModel( com.hormonetrack.data.model.PKModels.WHSAH)) - assertEquals(listOf("EV", "EU", "EEN"), + // v1.9.0 : ESE analytique (estrannaise.js) couvre 6 injectables, + // dont EUCS (undécylate suspension cristalline, exclusif à ce fit) ; + // l'ancienne couverture tables ODS n'était que EV/EU/EEn + assertEquals(listOf("EV", "EU", "EEN", "EC", "EB", "EUCS"), com.hormonetrack.data.model.Esters.choicesForModel( com.hormonetrack.data.model.PKModels.ESTRANNAISE)) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c3c6f4b..ce46a84 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,44 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …). +## [1.9.0] — 2026-09-18 (versionCode 38) + +### Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC + +- **Le modèle ESE n'est plus les tables horaires ODS** : il est désormais la + **forme close 3C publiée par estrannaise.js** (les sources originelles + dont l'ODS n'était que l'échantillonnage horaire) — fidélité épinglée : + **RMS 0,00** vs les tables sur 0→200 j, écart de pic ≤ 0,1 %. +- **Décision** (utilisatrice) : « tous les modèles ont maintenant leurs + sources originelles — on abandonne complètement les liens avec le fichier + ODS ». Concrètement : plus aucune lecture des tables au runtime + (`concentrationOfDose` dispatche ESE → forme close ; `cutoffHours` ESE → + 10 × t½ terminale analytique ; l'asset `pk_profiles.json` quitte l'APK, + −550 Ko, et ne sert plus qu'aux tests de fidélité). +- **6 esters injectables couverts** (EV, EU, EEn, EC, EB + **EUCS**, + undécylate suspension cristalline, exclusif à ce fit) — l'ancien ESE + n'en couvrait que 3. TFS reste à 7 (liste explicite : EUCS n'a pas de + V3C TFS), WHSAH à 6. +- **NUAGE D'INCERTITUDE (exclusif ESE)** : Estrannaise publie le posterior + MCMC de ses paramètres (313 échantillons/ester, asset ≈ 48 Ko) — chip + `Nuage` (off par défaut, activable à volonté, actif SEULEMENT si ESE est + affiché, coupure automatique si ESE est éteint) → nuage diffus de 32 + courbes du posterior montrant la plage d'imprécision du modèle, comme + sur le site estrannaise. Dessiné en alpha faible sous les courbes, hors + échelles/labels/extrema. +- t½ terminale ESE désormais **analytique** (ln2/min(k1,k2,k3)) — la + recommandation de prise de sang n'estime plus la t½ par pente de table. +- **11 nouveaux tests** (234 au total / 204 sans les données locales) : + fidélité (RMS, pics ±0,2 %), cas dégénérés (taux égaux → limites sans + NaN), MCMC 313 × 6, nuage (32 courbes à dispersion réelle, fenêtre, + exclusivité ESE, gardes), choicesForModel ESE=6, t½ analytique. +- **Porté web** (web v1.9.0, versions sync) : `estrannaise-models.js` + + `estrannaise-cloud.js` miroirs, nuage canvas, fetch MCMC au démarrage + (l'ODS web quitte aussi le runtime), 11 tests miroirs + E2E nuage. +- Validé émulateur §16.ter (APK release, seed réel v1.7.0) : courbe ESE + analytique affichée, chips Extend/Cloud présents, recommandation + de prise de sang fonctionnelle, 0 crash. + ## [1.8.2] — 2026-09-17 (versionCode 37) ### Maintenance — performance du moteur + fraîcheur de l'interface diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index d36ba61..1da7760 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -15,7 +15,7 @@ 4. [Environnement de build (cette machine)](#4-environnement-de-build-cette-machine) 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) -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.11 Prise de sang recommandée](#711-recommandation-de-prochaine-prise-de-sang-v180--pklabtimingkt) +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.11 Prise de sang recommandée](#711-recommandation-de-prochaine-prise-de-sang-v180--pklabtimingkt) + [7.12 Estrannaise analytique](#712-modèle-estrannaise-analytique--nuage-mcmc-v190--pkestrannaisemodelskt-pkestrannaisecloudk t) 8. [Tests unitaires](#8-tests-unitaires) + [8.bis Données de test réelles : 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) 10. [UI & navigation](#10-ui--navigation) @@ -63,6 +63,7 @@ Fonctionnalités v1 : | Date | Événement | |---|---| +| 17 sept. 2026 (session v1.9.0) | **Modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC (Android + web sync)** (demande) : découverte des sources originelles (github.com/WHSAH/estrannaise.js — forme close 3C + paramètres publiés + posterior MCMC 313/ester). Vérification numérique AVANT débranchement : **RMS 0,00 vs tables ODS** (l'ODS était l'échantillonnage de ces formules). ESE → analytique (dispatch, cutoff 10×t½, t½ LabTiming), PKProfileStore retiré du runtime (asset ODS → src/test/assets, −550 Ko d'APK), ESE couvre 6 esters (+EUCS), TFS = liste explicite (EUCS sans V3C TFS). **Nuage MCMC exclusif ESE** : chip `Nuage` (off, activable si ESE affiché, coupure auto si ESE off), 32 courbes du posterior déterministes, alpha faible sous les courbes, hors échelles/labels/extrema. 11 nouveaux tests (234 verts / 204 sans données locales) + lint + check web (166). Validé émulateur §16.ter : courbe ESE analytique affichée, unités, chips Extend/Cloud, 0 crash. | | 17 sept. 2026 (session v1.8.2) | **Maintenance — audit complet code + doc** : (a) **perf moteur** : cutoffHours pré-calculé par traitement dans e2At/computeCurve (le chemin chaud du tracé labs recalculait ~430 k appels/refresh), Bateman paresseux (inutile pour les traitements à profil), doses pré-groupées par traitement — résultat identique (ordre de sommation préservé, 223 tests verts inchangés) ; (b) **2 fixes de fraîcheur** (#65 accueil : clés produceState sans allTreatments/tConfig — éditer un scaleFactor n'actualisait pas la courbe avant le tick ; #66 graphiques : clé `labResults.size` — éditer la valeur d'un lab ne recalculait pas) ; (c) **nettoyage** : ~9 imports morts, qualifications raccourcies, code mort (Repository/DAO, currentLevel, scheduler2), médiane factorisée (4 copies), SimpleDateFormat hors boucle, AppLog sans recopie, Regex précompilées, formatDose dédupliqué ; (d) **doc** : ~35 corrections (compteurs actuels 223/193 partout, TOC + ancres réparées, inventaire §8 à jour et fusionné, doublons CHANGELOG, §19 backup, fautes). Audit préalable par double exploration lecture-seule (46 fichiers + 4 docs). 223 verts + lint + émulateur smoke. Android seul (web non concerné — re-render complet, aucune release web). | | 17 sept. 2026 (session v1.8.1) | **Fix stabilisation + suggestion sur l'Accueil** (critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app me disait stabilisée depuis février ») : le proxy « 1ʳᵉ dose du traitement » était aveugle aux changements récents. NOUVELLE règle `regimeStartMs` : le régime courant = la **séquence terminale de doses** à (ester effectif, dose mg, **écart inter-doses**) constants (l'écart comparé EXACTEMENT — 7 j ± 1 h casse le régime) ; tout changement récent réinitialise la stabilisation → la reco saute au premier creux post-stabilisation ; l'ester EFFECTIF de la dernière dose (override compris) alimente la carte. **+ Suggestion aussi sur l'Accueil** (demande) : carte compacte (creux daté + créneau, mention de stabilisation seulement si le régime n'était pas déjà stable). 3 nouveaux tests (223 verts / 193 sans données locales). Validé émulateur (release, seed réel) : la carte passe de « stabilisé depuis le 06/02 » à « EEN pas stabilisé avant le 29/09 — premier creux fiable », creux 27/09 22:43, 0 crash. Web v1.8.1 sync (3 tests miroirs + encart Accueil). | | 17 sept. 2026 (session v1.8.0) | **Recommandation de prochaine prise de sang (page Analyses, Android + web sync)** (demande + choix validés) : carte « Prochaine prise de sang (suggestion) » — creux exact calculé sur la courbe prévisionnelle **juste avant l'injection suivante** (+ mention « ou la veille du créneau »), **saut au premier creux STABILISÉ** (5 × t½ terminale — pas de prise intermédiaire trompeuse), filtres honnêtes (injectable actif + Posologie requis, creux jamais déjà mesuré, horizon borné → carte cachée), **invite « renseigne une Posologie »** quand un injectable actif en est dépourvu (demande explicite). Moteur : `pk/LabTiming.kt` PUR (miroir web `js/pk/lab-timing.js`) ; `PKProfileStore.terminalHalfLifeDays` (refactor de la pente d'extrapolation de `sample()` — le modèle Estrannaise lit sa t½ dans la table, TFS/WHSAH analytiquement). UI : calcul `produceState` + tick minute, disclaimer, mutuellement exclusive avec l'invite. 11 nouveaux tests (220 verts / 190 sans données locales) + lint + check web (152). Validé émulateur §16.ter (release, seed réel v1.7.0) : « creux 20/09 10:37, injection EEN 20/09 11:20, EEN stabilisé depuis le 06/02 », 0 crash. | @@ -643,9 +644,101 @@ prévisionnel STABILISÉ** — deux idées pharmacocinétiques : disclaimer. Stabilisation hors horizon (ester ultra-long) → carte cachée. - 11 tests `LabTimingTest` (8 + 3 v1.8.1 régime récent) + 1 test régression n°5. +### 7.12 Modèle Estrannaise ANALYTIQUE + nuage MCMC (v1.9.0) — `pk/EstrannaiseModels.kt`, `pk/EstrannaiseCloud.kt` + +**DÉCISION v1.9.0** (utilisatrice) : « tous les modèles ont maintenant leurs +sources originelles — on abandonne complètement les liens avec le fichier +ODS, qui lui-même était une extrapolation de ces mêmes sources ». TFS +(v1.4.0) et WHSAH (v1.4.6) étaient déjà analytiques ; **l'Estrannaise le +devient à son tour**. + +- **SOURCE** : https://github.com/WHSAH/estrannaise.js/ (site estrannaise) — + modèle 3 compartiments `dB/dt=−k1·B ; dEE/dt=k1·B−k2·EE ; dE2/dt=k2·EE−k3·E2` + en forme close : `C(t) = dose·d·k1·k2·[e^(−k1t)/((k1−k2)(k1−k3)) − + e^(−k2t)/((k1−k2)(k2−k3)) + e^(−k3t)/((k1−k3)(k2−k3))]` (t en JOURS). + Paramètres publiés par ester : EV [478, 0,236, 4,85, 1,24], EU + [471,5, 0,01729, 6,528, 2,285], EEn [191,4, 0,119, 0,601, 0,402], EC + [246, 0,0825, 3,57, 0,669], EB [1893,1, 0,67, 61,5, 4,34], EUCS + [16,15, 0,046, 0,022, 0,101] (nouveau code ester v1.9.0 : undécylate + suspension cristalline « EUn casubq »). +- **FIDÉLITÉ (vérifiée AVANT débranchement)** : la forme close reproduit les + tables ODS **RMS 0,00** sur 0→200 j (EV/EU/EEn), écart de pic ≤ 0,1 % — + l'ODS n'était que l'échantillonnage horaire de ces formules. Épinglé par + `EstrannaiseModelsTest` (Android) et `estrannaise-models.test.js` (web). +- **Débranchement runtime** : `concentrationOfDose` dispatche ESE → + [EstrannaiseModels.sample] (plus de fallback + [PKProfileStore.sample]) ; `cutoffHours` ESE → 10 × t½ terminale + analytique (plus la longueur de table) ; [LabTiming] t½ ESE analytique ; + [PKProfileStore.init] retirée du démarrage. **L'asset `pk_profiles.json` + quitte `src/main/assets` (−550 Ko d'APK) pour `src/test/assets/`** : + l'ODS survit UNIQUEMENT comme référence des tests de fidélité. + [PKProfileStore] est conservé (lecteur pur) pour ces tests. +- **Couverture étendue** : ESE passe de 3 à **6 esters injectables** + (EV/EU/EEn/EC/EB/**EUCS**) — `Esters.choicesForModel` mis à jour (TFS + reste 7, WHSAH 6 ; ⚠️ TFS = liste EXPLICITE : EUCS n'a pas de V3C TFS). +- **NUAGE D'INCERTITUDE (exclusif ESE, demande v1.9.0)** : Estrannaise + publie le **posterior MCMC** de ses paramètres (313 échantillons + `(d, k1, k2, k3)` par ester — asset `mcmc_samples.json` ≈ 48 Ko, chargé + au démarrage). Chip `Nuage` du graphique (off par défaut, activable à + volonté, ACTIVABLE SEULEMENT si ESE est affiché — éteindre ESE coupe le + nuage) → [EstrannaiseCloud.compute] trace 32 courbes du posterior + (échelonnées, déterministes) superposant les doses des porteurs ESE. + Dessin en alpha faible SOUS les courbes ; hors échelles/labels/extrema + (une plage d'imprécision, pas des données à cadrer). Limites : les + contributions TFS/WHSAH ne sont pas dans le nuage (pas de posterior + publié pour eux) ; les patchs du repo (tw/ow) ne sont pas portés. +- **Web sync** : `js/pk/estrannaise-models.js` + `js/pk/estrannaise-cloud.js` + miroirs ; le fetch des tables ODS au démarrage web est remplacé par le + fetch MCMC (démarrage plus rapide) ; nuage dessiné par `chart-canvas.js`. + +### 7.12 Modèle Estrannaise ANALYTIQUE + nuage MCMC (v1.9.0) — `pk/EstrannaiseModels.kt`, `pk/EstrannaiseCloud.kt` + +**DÉCISION v1.9.0** (utilisatrice) : « tous les modèles ont maintenant leurs +sources originelles — on abandonne complètement les liens avec le fichier +ODS, qui lui-même était une extrapolation de ces mêmes sources ». TFS +(v1.4.0) et WHSAH (v1.4.6) étaient déjà analytiques ; **l'Estrannaise le +devient à son tour**. + +- **SOURCE** : https://github.com/WHSAH/estrannaise.js/ — modèle 3 + compartiments en forme close : `C(t) = dose·d·k1·k2·[e^(−k1t)/((k1−k2)(k1−k3)) + − e^(−k2t)/((k1−k2)(k2−k3)) + e^(−k3t)/((k1−k3)(k2−k3))]` (t en JOURS). + Paramètres publiés par ester : EV [478, 0,236, 4,85, 1,24], EU [471,5, + 0,01729, 6,528, 2,285], EEn [191,4, 0,119, 0,601, 0,402], EC [246, + 0,0825, 3,57, 0,669], EB [1893,1, 0,67, 61,5, 4,34], EUCS [16,15, + 0,046, 0,022, 0,101] (nouveau code ester v1.9.0 : « EUn casubq »). +- **FIDÉLITÉ (vérifiée AVANT débranchement)** : la forme close reproduit + les tables ODS **RMS 0,00** sur 0→200 j (EV/EU/EEn), écart de pic + ≤ 0,1 % — l'ODS n'était que l'échantillonnage horaire de ces formules. + Épinglé par `EstrannaiseModelsTest` (Android) et + `estrannaise-models.test.js` (web). +- **Débranchement runtime** : `concentrationOfDose` dispatche ESE → + `EstrannaiseModels.sample` (plus de fallback `PKProfileStore.sample`) ; + `cutoffHours` ESE → 10 × t½ terminale analytique (plus la longueur de + table) ; `LabTiming` t½ ESE analytique ; `PKProfileStore.init` retirée + du démarrage. **L'asset `pk_profiles.json` quitte `src/main/assets` + (−550 Ko d'APK) pour `src/test/assets/`** : l'ODS survit UNIQUEMENT + comme référence des tests de fidélité. `PKProfileStore` est conservé + (lecteur pur) pour ces tests. +- **Couverture étendue** : ESE passe de 3 à **6 esters injectables** + (EV/EU/EEn/EC/EB/**EUCS**) — `Esters.choicesForModel` mis à jour (TFS + reste 7, WHSAH 6 ; ⚠️ TFS = liste EXPLICITE : EUCS n'a pas de V3C TFS). +- **NUAGE D'INCERTITUDE (exclusif ESE, demande v1.9.0)** : Estrannaise + publie le **posterior MCMC** de ses paramètres (313 échantillons par + ester — asset `mcmc_samples.json` ≈ 48 Ko, chargé au démarrage). Chip + `Nuage` du graphique (off par défaut, activable à volonté, ACTIVABLE + SEULEMENT si ESE est affiché — éteindre ESE coupe le nuage) → + `EstrannaiseCloud.compute` trace 32 courbes du posterior (échelonnées, + déterministes) superposant les doses des porteurs ESE. Dessin en alpha + faible SOUS les courbes ; hors échelles/labels/extrema. Limites : les + contributions TFS/WHSAH ne sont pas dans le nuage (pas de posterior + publié) ; les patchs du repo (tw/ow) ne sont pas portés. +- **Web sync** : `js/pk/estrannaise-models.js` + `js/pk/estrannaise-cloud.js` + miroirs ; le fetch des tables ODS au démarrage web est remplacé par le + fetch MCMC (démarrage plus rapide) ; nuage dessiné par `chart-canvas.js`. + ## 8. Tests unitaires -**223 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **193 sans +**234 tests JVM, tous verts** (`./gradlew testDebugUnitTest`) — **193 sans les données de test locales** (cf §8.bis : les 5 classes de régression, 6/6/6/5/7 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 = @@ -807,6 +900,14 @@ Emplacement : `app/src/test/java/com/hormonetrack/`. Répertoire de travail d'ex sans Posologie ou sans doses, agrégat multi-traitements (le plus tôt gagne), traitements inactifs/désactivés → null. +- **`EstrannaiseModelsTest`** (6) : v1.9.0 — modèle Estrannaise analytique : + FIDÉLITÉ aux tables ODS (maxAbs ≤ 0,01 / RMS ≤ 0,01 sur 0→200 j), pics + publiés reproduits (±0,2 %), cas dégénérés (taux égaux → limites sans + NaN), MCMC 313 × 6 esters (plage couverte), t½ analytique EV, + normalisation par mg + gardes (EUCS couvert). +- **`EstrannaiseCloudTest`** (5) : v1.9.0 — nuage d'incertitude : 32 courbes + à dispersion réelle (les posterior couvrent une plage), fenêtre respectée, + exclusivité ESE (TFS/oral → vide), sans dose → vide, nbCurves < 2 → vide. - **`LabTimingTest`** (11) : v1.8.0/v1.8.1 — recommandation de prochaine prise de sang : creux juste avant le créneau, saut au premier creux STABILISÉ (5 × t½ ; régime non stabilisé → creux suivant), prise récente → @@ -1632,6 +1733,19 @@ valeur d'un lab ne rafraîchissait pas les courbes avant le tick de 60 s. Leçon les clés d'un producteur d'état Compose doivent couvrir TOUTE entrée du calcul (pas un proxy comme `.size`). +## 15.bis Comment régénérer l'asset mcmc_samples.json (v1.9.0) + +L'asset `src/main/assets/mcmc_samples.json` (posterior MCMC de +estrannaise.js : 313 échantillons `(d, k1, k2, k3)` par ester injectable) +est extrait de `src/modeldata.js` du dépôt +https://github.com/WHSAH/estrannaise.js/ : +1. cloner le dépôt (branche main) ; +2. convertir les exports JS en JSON (node : importer `mcmcSamplesPK`, dump JSON) ; +3. mapper les clés du repo vers les clés de l'app : « EV im »→EV, « EUn im »→EU, + « EEn im »→EEn, « EC im »→EC, « EB im »→EB, « EUn casubq »→EUCS ; +4. écrire `{ester: [[d,k1,k2,k3] × 313]}` (compact, ≈ 48 Ko) ; +5. le miroir web lit `assets/mcmc_samples.json` du dépôt web (même fichier). + ## 15. Comment régénérer l'asset pk_profiles.json Si le `.ods` change (re-fits, nouveaux esters) : diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 48f0a8a..ecff557 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -45,8 +45,9 @@ installer — pas besoin d'Android Studio ni de faire un build : 1. Android Studio → **Open** → sélectionne le dossier `~/projects/HormoneTrack` 2. Laisse le **Gradle Sync** se terminer (première fois : téléchargements, 5–15 min) - La barre du bas affiche la progression ; attends « Gradle sync finished ». -3. Vérifie que l'asset est bien présent : - `app/src/main/assets/pk_profiles.json` (profils Estrannaise / Transfem Science extraits de ton `Estrogen.ods`). +3. (v1.9.0) Le modèle Estrannaise est **analytique** : l'ancienne table + `pk_profiles.json` n'est plus embarquée dans l'APK (elle ne sert qu'aux + tests de fidélité, dans `app/src/test/assets/`). ## 3. Préparer ton téléphone @@ -157,7 +158,10 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei ## 7. Les modèles mathématiques -- **Estrannaise** : tables horaires issues de ta feuille `Estrogen.ods` (table « Models ») +- **Estrannaise (v1.9.0 : analytique)** : forme close 3C publiée par + estrannaise.js — les anciennes tables du tableur en étaient + l'échantillonnage (fidélité vérifiée à l'identique) ; 6 esters injectables + couverts (EV, EU, EEn, EC, EB + EUCS en suspension cristalline) - **Transfem Science** (v1.4.0) : la méta-analyse officielle à 3 compartiments ([article](https://transfemscience.org/articles/injectable-e2-meta-analysis/), [simulateur](https://transfemscience.org/misc/injectable-e2-simulator/)) — @@ -214,6 +218,10 @@ Pics de référence (dose unique de 5 mg, IM) : - **Fuseau horaire du graphique** (v1.4.5) : Paramètres → « Fuseau horaire du graphique » — les jours s'alignent sur minuit du fuseau choisi (vide = celui du téléphone) +- **Nuage d'incertitude (v1.9.0, exclusif ESE)** : chip `Nuage` (actif + seulement quand Estrannaise est affiché) → nuage diffus de courbes du + posterior MCMC d'Estrannaise montrant la plage d'imprécision du modèle + (comme sur le site estrannaise) - **Unités sur les axes** (v1.7.0) : l'axe gauche affiche « pg/mL » (E2) et l'axe droit « ng/mL » (T, avec le toggle T) au sommet des colonnes de labels — plus besoin de deviner l'unité des nombres