fix(LM-1004): le recrédit appartient à la passe qui dépense du surplus

La règle disait « attribué à la PREMIÈRE passe où la charge apparaît », et le code
l'appliquait fidèlement. Mais la passe éco n'est pas bornée par le surplus : elle
l'est par l'autorisation de soutirage, et ne touche jamais remainingSurplusW. Lui
attribuer le recrédit revenait donc à ne le créditer NULLE PART.

Conséquence : une charge servie en éco ne récupérait jamais sa propre consommation,
et son tirage déprimait le budget dont elle avait besoin pour recevoir du surplus
par-dessus. Mesuré sur .75 : borne à 3 899 W, surplus publié −3 148 W ; sans elle
il valait +700 W. Elle se privait exactement du surplus qu'elle pouvait recevoir,
et « minimum + surplus » ne servait jamais que le minimum.

LE DÉFAUT PRÉEXISTAIT au lot d'aujourd'hui — le chauffe-eau servi en éco l'avait
déjà. Il était invisible parce que son compteur affiche 0 W : une charge qui ne
mesure rien ne peut pas se priver de quoi que ce soit. Il a fallu une charge qui
mesure, et qui tire quatre kilowatts, pour qu'il se voie. C'est une septième forme
de l'homogénéité du banc, et la première où c'est l'ABSENCE de mesure qui masque et
non l'uniformité.

Le paramètre recreditDejaAttribue et l'ensemble recreditAttribue sont retirés
plutôt que laissés inertes : un mécanisme vestigial se relit comme une garde.

Suites : loadmodel 26/26, simulation 67/67, charging 48/48, spotmarket 32/32.
This commit is contained in:
Patrick Schurig 2026-09-07 13:17:56 +02:00
parent 049eaeeaa1
commit 421e0452cc
3 changed files with 39 additions and 21 deletions

View File

@ -246,7 +246,6 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
// donc après un éco de rang 3 — c'est le sens même des deux passes, et le publier sous forme
// de niveaux est ce qui rend cet ordre lisible sans qu'un client ait à le redériver.
QHash<QString, LoadAction::Level> niveauxEco;
QSet<QString> recreditAttribue;
for (const LoadContext &lc : nonEvLoads) {
const EcoProgress avancement = m_arbitrator->ecoProgress(lc, ctx.timestamp);
@ -358,10 +357,6 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
eco.reason = { DecisionCode::EcoFloorGrid, motif };
niveauxEco.insert(lc.id, eco);
// LM-1004 — le recrédit est attribué à la PREMIÈRE passe où la charge apparaît, et le
// champ le rend vérifiable plutôt que discipliné : la discipline ne se teste pas.
recreditAttribue.insert(lc.id);
draw.committedW += achatW;
if (margeRestante.borne)
margeRestante.watts -= achatW;
@ -373,8 +368,8 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW, compta));
else
slot.actions.append(emitSetpointAction(
decideSetpoint(lc, remainingSurplusW, margeRestante, compta, draw,
recreditAttribue.contains(lc.id)))); // etmvariableload + relay-router + evcharger
decideSetpoint(lc, remainingSurplusW, margeRestante, compta, draw)));
// etmvariableload + relay-router + evcharger
// Le décrément vaut pour TOUTES les charges servies, bornes et PAC comprises : elles
// consomment la même autorisation, qu'elles aient été bornées par elle ou non. Ne
@ -515,8 +510,7 @@ DecisionReason RuleBasedScheduler::choisirMotif(const MotifEntrees &e) const
RuleBasedScheduler::SetpointTarget RuleBasedScheduler::decideSetpoint(const LoadContext &lc,
double &remainingSurplusW,
DrawMargin &margeRestante,
PlanBudget &budget, PlanDraw &draw,
bool recreditDejaAttribue) const
PlanBudget &budget, PlanDraw &draw) const
{
// Recrédit anti-clignotement (Correction B / contrat rév. 2 §5) : la conso de DÉBUT de cycle
// est déjà soustraite de l'export mesuré → on la rend au budget local avant d'arrondir.
@ -539,8 +533,22 @@ RuleBasedScheduler::SetpointTarget RuleBasedScheduler::decideSetpoint(const Load
// rembourser, et les watts prêtés sont réalloués aux suivantes pendant que la charge
// continue de les tirer. L'identité comptable TIENT malgré tout : aucun contrôle de
// cohérence ne peut l'attraper, c'est l'hypothèse qui est fausse, pas l'arithmétique.
// LM-1004 CORRIGÉE (2026-09-07) — le recrédit appartient à la passe qui DÉPENSE DU SURPLUS.
//
// La règle disait « à la première passe où la charge apparaît ». Or la passe éco ne dépense
// pas de surplus : elle est bornée par l'autorisation de soutirage, et ne touche jamais
// `remainingSurplusW`. Lui « attribuer » le recrédit revenait donc à ne le créditer NULLE
// PART — une charge servie en éco ne récupérait jamais sa propre consommation, et son
// tirage déprimait le budget dont elle avait besoin pour recevoir du surplus par-dessus.
//
// Mesuré sur .75 le 2026-09-07 : borne à 3 899 W, surplus publié −3 148 W. Sans la borne,
// il valait +700 W. Elle se privait exactement du surplus qu'elle pouvait recevoir, et le
// mode « minimum + surplus » ne servait jamais que le minimum.
//
// Le défaut préexistait pour le chauffe-eau servi en éco ; il était invisible parce que son
// compteur affiche 0 W — une charge qui ne mesure rien ne peut pas se priver de rien.
const bool refuse = lc.telemetry.commandRefused;
const double recreditW = (m_reserveActive || recreditDejaAttribue || refuse)
const double recreditW = (m_reserveActive || refuse)
? 0.0 : lc.telemetry.currentPowerW;
const double budgetW = remainingSurplusW + recreditW;
budget.recreditedW += recreditW;

View File

@ -83,11 +83,6 @@ private:
*
* \param lc Charge \c etmvariableload du SurplusContext.
* \param[in,out] remainingSurplusW Budget de surplus restant (W) ; mis à jour pour la suite.
* \param recreditDejaAttribue Vrai si la passe ÉCO a déjà crédité cette charge. Le recrédit
* anti-clignotement s'attribue à la PREMIÈRE passe où la charge apparaît et une seule fois
* (\rule{LM-1004}) — le recréditer deux fois rendrait au budget de surplus une puissance
* que l'éco a financée au réseau. Champ EXPLICITE plutôt que discipline : la discipline ne
* se teste pas.
* \param[in,out] margeRestante Autorisation de SOUTIRAGE restante (W), consommée le long
* de la cascade au même titre que le surplus. \c borne à faux = aucun plafond en vigueur,
* ce qui n'est PAS une marge nulle : sans plafond la cascade n'est pas bornée, à marge
@ -166,8 +161,7 @@ private:
*/
SetpointTarget decideSetpoint(const LoadContext &lc, double &remainingSurplusW,
DrawMargin &margeRestante,
PlanBudget &budget, PlanDraw &draw,
bool recreditDejaAttribue = false) const;
PlanBudget &budget, PlanDraw &draw) const;
/*!
* \brief Forme la commande à partir d'une cible décidée — et forme le motif.

View File

@ -558,10 +558,26 @@ C'est la règle qui empêche le défaut le plus probable de cette conception : u
en passe 1 puis recommandé en passe 2, donc deux commutations dans le même cycle — du
court-cycling fabriqué par l'arbitre lui-même.
**LM-1004 — Le recrédit anti-clignotement s'attribue UNE fois.** La correction B rend à chaque
charge sa consommation de début de cycle avant d'arrondir. Avec deux passes, ce crédit doit
être attribué à la **première** passe où la charge apparaît, jamais aux deux : compté deux
fois, il fabriquerait du budget qui n'existe pas.
**LM-1004 — Le recrédit anti-clignotement appartient à la passe qui DÉPENSE DU SURPLUS.** La
correction B rend à chaque charge sa consommation de début de cycle avant d'arrondir. Avec deux
passes, ce crédit ne doit être compté qu'une fois : compté deux fois, il fabriquerait du budget
qui n'existe pas.
> **Formulation corrigée le 2026-09-07.** La règle disait « à la **première** passe où la charge
> apparaît ». C'était faux, et le code l'appliquait fidèlement : la passe éco n'est pas bornée
> par le surplus mais par l'**autorisation de soutirage**, et ne touche jamais
> `remainingSurplusW`. Lui « attribuer » le recrédit revenait donc à ne le créditer **nulle
> part** — une charge servie en éco ne récupérait jamais sa propre consommation.
>
> **Conséquence mesurée** : borne à 3 899 W, surplus publié **−3 148 W** ; sans elle il valait
> **+700 W**. Elle se privait exactement du surplus qu'elle pouvait recevoir, et « minimum +
> surplus » ne servait jamais que le minimum.
>
> Le défaut existait depuis le §10 pour le chauffe-eau servi en éco. Il était **invisible parce
> que son compteur affiche 0 W** : une charge qui ne mesure rien ne peut pas se priver de quoi
> que ce soit. Il a fallu une charge qui mesure — et qui tire quatre kilowatts — pour qu'il se
> voie. Septième forme de l'homogénéité du banc, et la première où c'est l'ABSENCE de mesure
> qui masque, non l'uniformité.
**LM-1005 — La frontière que ce modèle franchit, et il faut la nommer.**