diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.cpp b/energyplugin/etm/scheduler/rulebasedscheduler.cpp index 71091eb..2d9d016 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.cpp +++ b/energyplugin/etm/scheduler/rulebasedscheduler.cpp @@ -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 niveauxEco; - QSet 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; diff --git a/energyplugin/etm/scheduler/rulebasedscheduler.h b/energyplugin/etm/scheduler/rulebasedscheduler.h index 373dbd8..a42c8df 100644 --- a/energyplugin/etm/scheduler/rulebasedscheduler.h +++ b/energyplugin/etm/scheduler/rulebasedscheduler.h @@ -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. diff --git a/specs/spec_loadmodel.md b/specs/spec_loadmodel.md index 27c7f8b..d285126 100644 --- a/specs/spec_loadmodel.md +++ b/specs/spec_loadmodel.md @@ -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.**