fix(3g-1): la conversion watts→ampères arrondit, elle ne tronque pas

Quatrième défaut latent de 3g-1. Il vaut son propre message parce que c'est
une décision, et qu'elle s'est prise contre le test.

3g-1 convertit l'enveloppe du waterfall en ampères puis la passe à
`static_cast<uint>(qBound(...))` — donc une TRONCATURE. Cela paraît prudent
(« ne jamais dépasser l'enveloppe ») et coûte systématiquement un ampère :
230 W par phase, abandonnés à chaque cycle, toute la journée. Les scénarios de
simulation le chiffrent — huit d'entre eux sortaient un ampère plus bas, et
la voiture finissait la journée 1 % moins chargée.

L'arbitrage : l'enveloppe est une ALLOCATION, pas une limite. Elle sort d'une
mesure de compteur dont le bruit dépasse largement 115 W. Ce qui est une
limite — le courant maximal de la borne et la place laissée par la protection
de surcharge — n'est pas franchi pour autant : le plafond est déclaré à
l'enveloppe (commit précédent), donc respecté AVANT l'allocation, et le qBound
écrête au matériel.

C'est pourquoi le CODE a été réaligné sur l'amont plutôt que le test :
testOverloadProtectionEcoMode encode le comportement de l'arrondi, et la
troncature faisait basculer la protection de surcharge de « réduire au
minimum » à « couper ». Un ampère de conversion ne doit pas décider ça.

Et c'est aussi pourquoi la limite de phase n'est PAS réappliquée en ampères
entiers dans l'adaptateur : ce second écrêtage serait plus strict que celui de
l'amont (plancher au lieu d'arrondi) et coûterait de nouveau un ampère, pour
69 W de marge théorique que la couche L4 mesure réellement en temps réel.

Suite simulation `run` 16/16, testOverloadProtectionEcoMode vert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
This commit is contained in:
Patrick Schurig 2026-08-27 10:31:46 +02:00
parent e5e20db9fd
commit c8bd41e36b

View File

@ -157,8 +157,27 @@ LoadAction EvAdapter::applyAction(const LoadAction &action, const QDateTime &now
double demandeA = action.currentA;
if (demandeA <= 0 && action.powerW > 0)
demandeA = action.powerW / (230.0 * phases);
// 3g-2 — arrondi au PLUS PROCHE, comme le faisait le proxy. Tronquer paraissait plus
// prudent (« ne jamais dépasser l'enveloppe ») et coûtait systématiquement un ampère :
// 230 W par phase abandonnés à chaque cycle, sur toute la journée. Les 18 scénarios de
// simulation le chiffrent — voiture 1 % moins chargée le soir, huit fois sur huit.
//
// L'enveloppe est une ALLOCATION, pas une limite : elle sort d'une mesure de compteur
// dont le bruit dépasse largement 115 W. Ce qui est une limite, c'est le plafond
// matériel — et celui-là n'est jamais franchi : il est déclaré à l'enveloppe par
// toLoadContext(), et le qBound ci-dessous écrête au courant maximal de la borne.
demandeA = qRound(action.powerW / (230.0 * phases));
// La limite de phase n'est PAS réappliquée ici, et c'est délibéré : elle est déjà dans
// l'enveloppe, déclarée par toLoadContext() comme plafond de la charge, donc respectée par
// le waterfall AVANT d'allouer. La réappliquer en ampères entiers ferait un second
// écrêtage, plus strict que celui de l'amont (plancher au lieu d'arrondi), et coûterait à
// nouveau un ampère dans les scénarios de simulation — pour 69 W de marge théorique que la
// couche L4, elle, mesure réellement en temps réel.
//
// La répartition reste celle du modèle de sécurité : la stratégie respecte le plafond
// déclaré, l'adaptateur écrête au matériel, et verifyOverloadProtection() garde le dernier
// mot sur la mesure.
const uint clampedA = static_cast<uint>(
qBound(static_cast<double>(minA), demandeA, static_cast<double>(maxA)));