v1.9.1 : fix nuage calibré (il entoure la courbe ESE calibrée) + harmonisation arrondi (miroir Android)
- estrannaise-cloud.js : paramètre scalePerEster — le nuage reçoit la
MÊME calibration que la courbe ESE (autoByModel ESE). Avant : courbe ESE
calibrée mais nuage BRUT → échelles différentes (remontée v1.9.1 :
« le nuage ne s'active que autour du tracé, pas autour du modèle
Estrannaise »).
- chart.js : autoByModel.get('ESE')?.esterScales passé au nuage ;
DEFAULT_NB_CURVES importé.
- +1 test : nuage calibré ×2 = nuage brut ×2 (167 verts au total).
- CHANGELOG [1.9.1] : fix nuage calibré + HARMONISATION de l'arrondi de
la médiane de calibration (le web avait la troncature historique — les
niveaux web/android convergent ; le niveau correct = l'ARRONDI).
- WEB_VERSION 1.9.1.
This commit is contained in:
parent
174928bb6b
commit
fc1b6e1700
@ -5,6 +5,19 @@
|
|||||||
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
||||||
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
|
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
|
||||||
|
|
||||||
|
## [1.9.1] — 2026-09-19
|
||||||
|
|
||||||
|
### Corrigé — le nuage d'incertitude entoure maintenant la courbe Estrannaise calibrée
|
||||||
|
|
||||||
|
- Miroir du fix Android v1.9.1 : la courbe ESE affichée est calibrée
|
||||||
|
(scalePerEster des labs) mais le nuage était tracé brut → échelles
|
||||||
|
différentes. Le nuage reçoit maintenant le MÊME scalePerEster que la
|
||||||
|
courbe ESE.
|
||||||
|
- Harmonisation de l'arrondi de la médiane de calibration (le web avait
|
||||||
|
la troncature historique) : les niveaux estimés web et Android
|
||||||
|
convergent. Le niveau correct = l'ARRONDI (plus juste que la
|
||||||
|
troncature, cf CHANGELOG Android 1.9.1).
|
||||||
|
|
||||||
## [1.9.0] — 2026-09-18
|
## [1.9.0] — 2026-09-18
|
||||||
|
|
||||||
### Modèle Estrannaise ANALYTIQUE + nuage d'incertitude (portage Android v1.9.0, versions sync)
|
### Modèle Estrannaise ANALYTIQUE + nuage d'incertitude (portage Android v1.9.0, versions sync)
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as EstrannaiseModels from './estrannaise-models.js';
|
import * as EstrannaiseModels from './estrannaise-models.js';
|
||||||
import { usesProfileModel } from './pk-engine.js';
|
import { usesProfileModel, doseEster } from './pk-engine.js';
|
||||||
|
|
||||||
|
|
||||||
/** Nombre de courbes du nuage (compromis étendue visuelle / coût ×32). */
|
/** Nombre de courbes du nuage (compromis étendue visuelle / coût ×32). */
|
||||||
@ -45,7 +45,15 @@ export const MIN_HORIZON_HOURS = 6;
|
|||||||
* ESE couvert, aucune dose, ou MCMC non chargé.
|
* ESE couvert, aucune dose, ou MCMC non chargé.
|
||||||
*/
|
*/
|
||||||
export function compute(
|
export function compute(
|
||||||
treatments, doseLogs, startMs, endMs, stepMs, nowMs, nbCurves = DEFAULT_NB_CURVES,
|
treatments, doseLogs, startMs, endMs, stepMs, nowMs,
|
||||||
|
nbCurves = DEFAULT_NB_CURVES,
|
||||||
|
/**
|
||||||
|
* Calibration à appliquer au nuage (v1.9.1 — fix « le nuage ne s'active
|
||||||
|
* que autour du tracé, pas autour du modèle Estrannaise ») : passer le
|
||||||
|
* MÊME scalePerEster que la courbe ESE pour que le nuage ENTOURE la
|
||||||
|
* courbe calibrée.
|
||||||
|
*/
|
||||||
|
scalePerEster = null,
|
||||||
) {
|
) {
|
||||||
if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return [];
|
if (startMs >= endMs || stepMs <= 0 || nbCurves < 2) return [];
|
||||||
|
|
||||||
@ -91,9 +99,11 @@ export function compute(
|
|||||||
if (dose.timestamp > t) continue;
|
if (dose.timestamp > t) continue;
|
||||||
const dtH = (t - dose.timestamp) / 3600000;
|
const dtH = (t - dose.timestamp) / 3600000;
|
||||||
const tr = carriers.find((x) => x.id === dose.treatmentId);
|
const tr = carriers.find((x) => x.id === dose.treatmentId);
|
||||||
|
// Même calibration que la courbe ESE (fix v1.9.1)
|
||||||
|
const scale = scalePerEster?.[doseEster(tr, dose)] ?? tr.scaleFactor;
|
||||||
e2 += EstrannaiseModels.sampleWithParams(
|
e2 += EstrannaiseModels.sampleWithParams(
|
||||||
tr.esterType, dtH, paramsByEster.get(tr.esterType),
|
tr.esterType, dtH, paramsByEster.get(tr.esterType),
|
||||||
) * dose.doseAmount;
|
) * dose.doseAmount * scale;
|
||||||
}
|
}
|
||||||
curve.push({ timestamp: t, e2, t: 0.0 });
|
curve.push({ timestamp: t, e2, t: 0.0 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
computeCurve, generateForecastDoses, autoCalibrated, TConfig,
|
computeCurve, generateForecastDoses, autoCalibrated, TConfig,
|
||||||
computeLabAnchoredCurve, computeEstrannaiseCloud,
|
computeLabAnchoredCurve, computeEstrannaiseCloud, DEFAULT_NB_CURVES,
|
||||||
} from '../pk/index.js';
|
} from '../pk/index.js';
|
||||||
import {
|
import {
|
||||||
stepForRange, defaultModelToggles, panDeltaHours, clampPanHours,
|
stepForRange, defaultModelToggles, panDeltaHours, clampPanHours,
|
||||||
@ -314,6 +314,7 @@ export function renderChart(container, ctx, { preserveState = false } = {}) {
|
|||||||
if (state.showCloud) {
|
if (state.showCloud) {
|
||||||
const cloud = computeEstrannaiseCloud(
|
const cloud = computeEstrannaiseCloud(
|
||||||
treatments, doseLogs, startMs, endMs, step, nowMs,
|
treatments, doseLogs, startMs, endMs, step, nowMs,
|
||||||
|
DEFAULT_NB_CURVES, autoByModel.get('ESE')?.esterScales ?? null,
|
||||||
);
|
);
|
||||||
if (cloud.length > 0) curves.push(['CLOUD', cloud.flat()]);
|
if (cloud.length > 0) curves.push(['CLOUD', cloud.flat()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { el, showDialog, field, switchRow, filterChip, toast } from './component
|
|||||||
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
||||||
* v1.4.10 = portage initial de l'Android v1.4.10.
|
* v1.4.10 = portage initial de l'Android v1.4.10.
|
||||||
*/
|
*/
|
||||||
export const WEB_VERSION = '1.9.0';
|
export const WEB_VERSION = '1.9.1';
|
||||||
|
|
||||||
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
||||||
function parseNumOrNull(text) {
|
function parseNumOrNull(text) {
|
||||||
|
|||||||
@ -131,7 +131,7 @@ async function main() {
|
|||||||
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
ok(dialog !== null, 'dialog « Nouveautés » au premier lancement (markdown rendu)');
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
const dlgText = await dialog.textContent();
|
const dlgText = await dialog.textContent();
|
||||||
ok(dlgText.includes('Version 1.9.0'), 'dialog : section de la version courante');
|
ok(dlgText.includes('Version 1.9.1'), 'dialog : section de la version courante');
|
||||||
const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
|
const hasRenderedMd = await dialog.$('.md-body h3, .md-body strong');
|
||||||
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
ok(hasRenderedMd !== null, 'dialog : markdown RENDU (titres/gras), pas de brut');
|
||||||
await page.click('.dialog .actions button');
|
await page.click('.dialog .actions button');
|
||||||
@ -334,7 +334,7 @@ async function main() {
|
|||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
text = await page.textContent('main.content');
|
text = await page.textContent('main.content');
|
||||||
ok(text.includes('Langue'), 'Paramètres : carte langue');
|
ok(text.includes('Langue'), 'Paramètres : carte langue');
|
||||||
ok(text.includes('Version 1.9.0 (web)'), 'Paramètres : version affichée');
|
ok(text.includes('Version 1.9.1 (web)'), 'Paramètres : version affichée');
|
||||||
ok(text.includes('100 % local'), 'Paramètres : note vie privée');
|
ok(text.includes('100 % local'), 'Paramètres : note vie privée');
|
||||||
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
await page.screenshot({ path: join(SHOTS, 'e2e-settings.png') });
|
||||||
|
|
||||||
|
|||||||
@ -66,4 +66,20 @@ describe('EstrannaiseCloud web (miroir Android v1.9.0)', () => {
|
|||||||
test('nbCurves < 2 → nuage vide', () => {
|
test('nbCurves < 2 → nuage vide', () => {
|
||||||
assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0);
|
assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('le nuage suit la calibration (fix v1.9.1, miroir Android)', () => {
|
||||||
|
// scalePerEster ×2 → nuage calibré = nuage brut ×2 (le nuage ENTOURE
|
||||||
|
// la courbe ESE calibrée au lieu de flotter à une autre échelle)
|
||||||
|
const scaled = { EEN: 2.0 };
|
||||||
|
const calibrated = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 32, scaled);
|
||||||
|
const raw = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW);
|
||||||
|
assert.equal(calibrated.length, raw.length);
|
||||||
|
assert.ok(raw[raw.length - 1].length >= 2);
|
||||||
|
const rawLast = raw[raw.length - 1][raw[raw.length - 1].length - 1].e2;
|
||||||
|
const calLast = calibrated[calibrated.length - 1][calibrated[calibrated.length - 1].length - 1].e2;
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(calLast - rawLast * 2) <= rawLast * 0.01,
|
||||||
|
`calibré (${calLast}) ≈ 2× brut (${rawLast})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user