refactor(§10 étape A2): décider et émettre se séparent, sans rien déplacer
Tant que décider et émettre étaient le même geste, une seconde passe était littéralement inexprimable : chaque décision produisait immédiatement une action, et le budget se décrémentait dans le même mouvement. `decideSetpoint()` rend désormais une CIBLE ; `emitSetpointAction()` en forme la commande et le motif. La contrainte du lot est tenue et vérifiée sur le fichier produit : vouluW est capturé AVANT l'écrêtage de verrou (ECS-309), l'écrêtage précède le décrément du budget (ECS-306), et les deux restent dans la phase de DÉCISION. Un déplacement d'un cran publierait des motifs faux sans qu'aucun nombre ne bouge. Le remaniement est fait par ALIAS DE RÉFÉRENCE — `LoadAction &la = t.action` — précisément pour que les vingt lignes d'affectation ne soient pas retouchées. Ce qui n'est pas réécrit ne peut pas dériver. Et le typage a servi de garde-fou : `SetpointTarget` n'étant pas `LoadAction`, un `return` converti au mauvais endroit n'aurait pas compilé. Les trois sorties anticipées (PHASE_LIMIT, EV_GRID_START, LOAD_UNAVAILABLE) portent déjà leur motif : la cause est connue au moment où la décision s'arrête, il n'y a rien à choisir. Suite complète : simulation 54/54, charging 48/48 identique à la référence ligne à ligne, loadmodel 21/21, spotmarket 32/32. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
This commit is contained in:
parent
b2e1cf208d
commit
9fcc66fe0e
@ -181,7 +181,7 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
// Charges pilotables non-EV triées par priorité ASCENDANTE : rang 1 = premier servi
|
||||
// (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang). Le budget de surplus est
|
||||
// UNIQUE et cascade à travers TOUTES ces charges par priorité :
|
||||
// - etmvariableload (ECS/routeur, kind Setpoint W → buildSetpointAction, règle d'arrondi §5) ;
|
||||
// - etmvariableload (ECS/routeur, kind Setpoint W → decideSetpoint/emitSetpointAction, règle d'arrondi §5) ;
|
||||
// - sg-ready (PAC, kind State → buildSgReadyStateAction, mapping sémantique).
|
||||
// Charges en WATTS (Setpoint) : etmvariableload (continu) ET relay-router (rév. 3, combinaison
|
||||
// de relais dérivée côté routeur). Charge à ÉTATS : sg-ready (PAC). Le scheduler distingue
|
||||
@ -226,7 +226,8 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
if (lc.adapter == QStringLiteral("sg-ready"))
|
||||
slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW, compta));
|
||||
else
|
||||
slot.actions.append(buildSetpointAction(lc, remainingSurplusW, margeRestante, compta, draw)); // etmvariableload + relay-router + evcharger
|
||||
slot.actions.append(emitSetpointAction(
|
||||
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
|
||||
@ -339,7 +340,7 @@ DecisionReason RuleBasedScheduler::choisirMotif(const MotifEntrees &e) const
|
||||
return { DecisionCode::SurplusInsufficient, {{"budgetW", qRound(e.budgetW)}} };
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
RuleBasedScheduler::SetpointTarget RuleBasedScheduler::decideSetpoint(const LoadContext &lc,
|
||||
double &remainingSurplusW,
|
||||
DrawMargin &margeRestante,
|
||||
PlanBudget &budget, PlanDraw &draw) const
|
||||
@ -381,7 +382,8 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
// plancher » enverrait chercher du soleil là où il faut regarder l'abonnement
|
||||
// (règle 7-b). Le budget n'est pas consommé : il passe entier à la suivante.
|
||||
if (lc.declared.minPowerW > 0 && maxW < lc.declared.minPowerW) {
|
||||
LoadAction la;
|
||||
SetpointTarget t;
|
||||
LoadAction &la = t.action; // alias : les affectations ci-dessous ne bougent pas
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Setpoint;
|
||||
la.funding = LoadAction::Surplus;
|
||||
@ -394,7 +396,7 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
la.reason = { DecisionCode::PhaseLimit,
|
||||
{{"limitW", qRound(maxW)}, {"requiredW", qRound(lc.declared.minPowerW)}} };
|
||||
remainingSurplusW = budgetW; // rien d'engagé : le recrédit repart intact
|
||||
return la;
|
||||
return t;
|
||||
}
|
||||
|
||||
setpointW = qBound(0.0, budgetW, maxW);
|
||||
@ -430,7 +432,8 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
if (estUneBorne && tolerance > 0 && budgetW > 0 && budgetW >= plancherW * tolerance) {
|
||||
const double soutireW = plancherW - budgetW;
|
||||
|
||||
LoadAction la;
|
||||
SetpointTarget t;
|
||||
LoadAction &la = t.action; // alias : les affectations ci-dessous ne bougent pas
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Setpoint;
|
||||
// Le financement dit la VÉRITÉ sur l'origine : une partie de cette puissance
|
||||
@ -460,7 +463,7 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
budget.allocatedW += budgetW;
|
||||
draw.committedW += soutireW;
|
||||
remainingSurplusW = 0;
|
||||
return la;
|
||||
return t;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -545,7 +548,8 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
// Le clamp ci-dessous suffit donc à produire la bonne comptabilité, sans branche spéciale.
|
||||
if (!lc.telemetry.available) {
|
||||
const double gele = lc.telemetry.lockMinPowerW;
|
||||
LoadAction la;
|
||||
SetpointTarget t;
|
||||
LoadAction &la = t.action; // alias : les affectations ci-dessous ne bougent pas
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Setpoint;
|
||||
la.funding = LoadAction::Surplus;
|
||||
@ -558,7 +562,7 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
la.reason = { DecisionCode::LoadUnavailable, {{"frozenW", qRound(gele)}} };
|
||||
remainingSurplusW = budgetW - gele;
|
||||
budget.allocatedW += gele; // une puissance qu'on ne sait plus couper reste allouée
|
||||
return la;
|
||||
return t;
|
||||
}
|
||||
|
||||
// ECS-306 — clamp lock-aware AVANT de décrémenter le budget (AGENTS règle 4 : les
|
||||
@ -576,7 +580,8 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
if (setpointW < lc.telemetry.lockMinPowerW)
|
||||
setpointW = lc.telemetry.lockMinPowerW; // puissance engagée, non-coupable
|
||||
|
||||
LoadAction la;
|
||||
SetpointTarget t;
|
||||
LoadAction &la = t.action; // alias : les affectations ci-dessous ne bougent pas
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Setpoint;
|
||||
la.funding = LoadAction::Surplus;
|
||||
@ -601,13 +606,27 @@ LoadAction RuleBasedScheduler::buildSetpointAction(const LoadContext &lc,
|
||||
// cause. Constaté au banc le 2026-08-13, sept fois en quatre heures, toujours au cycle
|
||||
// suivant un palier 0. Les deux branches se lisent maintenant sur le même axe :
|
||||
// le verrou a relevé la consigne (minOn), ou il l'a rabaissée (minOff).
|
||||
la.reason = choisirMotif({ setpointW, vouluW, budgetW, plancherW,
|
||||
lc.telemetry.currentPowerW, margeRestante,
|
||||
sousPlancher, capEpuise, dynamic });
|
||||
// §10 / étape A2 — la DÉCISION s'arrête ici. Le motif se choisit à l'émission, sur ces
|
||||
// entrées-là : c'est ce qui permettra à deux passes de contribuer à UNE commande, chacune
|
||||
// avec son propre motif (R7). Rien d'autre ne bouge — et surtout pas l'ordre au-dessus.
|
||||
t.motifAChoisir = true;
|
||||
t.entrees = { setpointW, vouluW, budgetW, plancherW,
|
||||
lc.telemetry.currentPowerW, margeRestante,
|
||||
sousPlancher, capEpuise, dynamic };
|
||||
|
||||
// Résidu : budget − consigne engagée → charge suivante de la priorité (même cycle).
|
||||
remainingSurplusW = budgetW - setpointW;
|
||||
budget.allocatedW += setpointW;
|
||||
return t;
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::emitSetpointAction(const SetpointTarget &t) const
|
||||
{
|
||||
LoadAction la = t.action;
|
||||
// Les sorties anticipées (PHASE_LIMIT, EV_GRID_START, LOAD_UNAVAILABLE) portent DÉJÀ leur
|
||||
// motif : il n'y a rien à choisir, la cause est connue au moment où la décision s'arrête.
|
||||
if (t.motifAChoisir)
|
||||
la.reason = choisirMotif(t.entrees);
|
||||
return la;
|
||||
}
|
||||
|
||||
@ -623,7 +642,7 @@ LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc,
|
||||
// revient au budget — comme l'ECS. Base : "quel surplus si la PAC n'était pas pilotée ?"
|
||||
// 3g-2 — même exception que pour les charges en watts : sous la réserve, pas de recrédit.
|
||||
// Une PAC en état 3 ou 4 y resterait sinon indéfiniment, la réserve n'agissant que sur les
|
||||
// montées d'état. Voir la note détaillée dans buildSetpointAction().
|
||||
// montées d'état. Voir la note détaillée dans decideSetpoint().
|
||||
const double allocatedNowW = m_reserveActive
|
||||
? 0.0
|
||||
: lc.declared.estimatedPowerW.value(currentState, 0.0);
|
||||
|
||||
@ -93,7 +93,7 @@ private:
|
||||
* accumulés ici, dans la cascade même. Le recrédit y figure parce que sans lui
|
||||
* l'identité \c surplus + \c recrédit − \c alloué == \c restant serait fausse dès
|
||||
* qu'une charge consomme déjà — c'est-à-dire dans le cas normal.
|
||||
* \return LoadAction kind=Setpoint, funding=Surplus, \c powerW = consigne, \c reason non vide.
|
||||
* \return La cible décidée. Le motif est formé par \c emitSetpointAction().
|
||||
*/
|
||||
//! \param draw Registre des watts ACHETÉS (R3) — alimenté par EV_GRID_START, qui est le
|
||||
//! seul endroit de la cascade où le moteur sache partager une allocation entre le
|
||||
@ -128,10 +128,45 @@ private:
|
||||
*/
|
||||
DecisionReason choisirMotif(const MotifEntrees &e) const;
|
||||
|
||||
LoadAction buildSetpointAction(const LoadContext &lc, double &remainingSurplusW,
|
||||
/*!
|
||||
* \brief Ce qu'une passe a DÉCIDÉ pour une charge — avant que le motif ne soit formé.
|
||||
*
|
||||
* \par Pourquoi la décision et l'émission se séparent (§10, étape A2)
|
||||
* Deux passes devront contribuer à **une seule** commande (\rule{LM-1003}). Tant que
|
||||
* décider et émettre étaient le même geste, la seconde passe était littéralement
|
||||
* inexprimable : chaque décision produisait immédiatement une action.
|
||||
*
|
||||
* \warning Ce qui NE se déplace pas, et ne doit jamais l'être : \c vouluW est capturé
|
||||
* **avant** l'écrêtage de verrou (\c ECS-309, sinon le motif ne distingue plus « le
|
||||
* budget ne payait pas » de « le verrou a refusé »), et l'écrêtage précède le décrément
|
||||
* du budget (\c ECS-306, sinon les charges suivantes reçoivent un résidu surestimé).
|
||||
* Les deux vivent dans la phase de DÉCISION, et un déplacement d'un cran publierait des
|
||||
* motifs faux **sans qu'aucun nombre ne bouge**.
|
||||
*/
|
||||
struct SetpointTarget {
|
||||
LoadAction action; //!< Tout, sauf le motif quand il reste à choisir.
|
||||
bool motifAChoisir = false; //!< Faux pour les sorties anticipées, qui le portent déjà.
|
||||
MotifEntrees entrees; //!< Les grandeurs du choix, si \c motifAChoisir.
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Décide la consigne d'une charge et consomme les ressources du cycle.
|
||||
* \return La cible, motif compris pour les sorties anticipées.
|
||||
* \note C'est ici, et nulle part ailleurs, que le budget et l'autorisation sont entamés.
|
||||
*/
|
||||
SetpointTarget decideSetpoint(const LoadContext &lc, double &remainingSurplusW,
|
||||
DrawMargin &margeRestante,
|
||||
PlanBudget &budget, PlanDraw &draw) const;
|
||||
|
||||
/*!
|
||||
* \brief Forme la commande à partir d'une cible décidée — et forme le motif.
|
||||
* \param t Cible issue de \c decideSetpoint().
|
||||
* \return Le \c LoadAction émis, motif non vide.
|
||||
* \note Ne consomme aucune ressource : tout a été entamé à la décision. C'est ce qui rendra
|
||||
* possible d'émettre UNE commande à partir de DEUX passes.
|
||||
*/
|
||||
LoadAction emitSetpointAction(const SetpointTarget &t) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus.
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user