HormoneTrack-web/js/pk/pk-extrema.js
Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
Portage navigateur complet de l'app Android HormoneTrack v1.4.10 :
- moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C,
  WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android
  préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés
- calibration par période d'ester et par modèle affiché (#60/#61)
- UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire,
  prévision, pics/creux, fuseau configurable), rappels web
  (Notification API), seuils d'alerte, i18n FR/EN
- sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1)
- 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie
- processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur,
  smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG)
- dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions
  alignées sur l'Android porté, changelogs indépendants

Non porté (impossible dans un navigateur, documenté §12) : agenda
récurrent, notifications onglet fermé, montre.
2026-09-08 14:34:58 +02:00

75 lines
3.0 KiB
JavaScript

/**
* ─────────────────────────────────────────────────────────────────────────────
* Détection des pics et creux (partie 4/4 du moteur) — portage de
* `PharmacokineticEngine.detectExtrema` (v1.2.3) : triangles ▲▼ aux extrema
* locaux de chaque courbe, avec valeurs estimées (v1.2.6).
*
* PUR, testable en Node (web/tests/extrema.test.js).
* ─────────────────────────────────────────────────────────────────────────────
*/
/**
* Un extremum détecté : pic (isPeak = true) ou creux, avec sa valeur.
*/
export class Extremum {
/** @param {number} timestamp · @param {number} value · @param {boolean} isPeak */
constructor(timestamp, value, isPeak) {
this.timestamp = timestamp;
this.value = value;
this.isPeak = isPeak;
}
}
/**
* Détecte les maxima et minima LOCAUX d'une courbe (les pics après chaque
* injection et les creux juste avant la suivante, en régime d'équilibre).
*
* - les plateaux (valeurs égales consécutives) comptent pour UN seul extremum ;
* - les extrema consécutifs de même type sont FUSIONNÉS (le plus extrême gagne) ;
* - les oscillations d'amplitude < minSwingAbs sont ignorées (bruit
* d'arrondi) — utiliser ~2 pg/mL pour l'E2 et ~0,02 ng/mL pour la T.
*
* @param {import('./pk-engine.js').LevelPoint[]} points Points de courbe
* @param {(p:object)=>number} selector Ce qu'on analyse (`p => p.e2` ou `p => p.t`)
* @param {number} [minSwingAbs=2.0] Amplitude minimale d'une oscillation
* @returns {Extremum[]}
*/
export function detectExtrema(points, selector, minSwingAbs = 2.0) {
if (points.length < 3) return [];
const values = points.map(selector);
const abs = Math.abs;
// 1) extrema locaux stricts (un plateau = son premier point)
const raw = []; // [index, isPeak]
let i = 1;
while (i < values.length - 1) {
const v = values[i];
const prev = values[i - 1];
let j = i;
while (j < values.length - 1 && values[j + 1] === v) j++; // plateau
if (j >= values.length - 1) break;
const next = values[j + 1];
if (v > prev && v > next) {
raw.push([i, true]);
} else if (v < prev && v < next) {
raw.push([i, false]);
}
i = j + 1;
}
// 2) alternance pic/creux + filtre d'amplitude
const out = [];
for (const [idx, isPeak] of raw) {
const last = out[out.length - 1] || null;
if (last && last.isPeak === isPeak) {
// doublon de même type : garder le plus extrême
const better = isPeak ? values[idx] > last.value : values[idx] < last.value;
if (better) out[out.length - 1] = new Extremum(points[idx].timestamp, values[idx], isPeak);
continue;
}
if (last && abs(last.value - values[idx]) < minSwingAbs) continue;
out.push(new Extremum(points[idx].timestamp, values[idx], isPeak));
}
return out;
}