fix(mpc): replay every plan and represent a true zero PV cap - #971
fix(mpc): replay every plan and represent a true zero PV cap#971frahlg wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4cb4119a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| soc := p.InitialSoCPct | ||
| if soc < p.SoCMinPct { | ||
| soc = p.SoCMinPct | ||
| } |
There was a problem hiding this comment.
Replay from the actual out-of-band SoC
When live telemetry is below SoCMinPct (or above SoCMaxPct), this replaces the real starting SoC with the configured boundary before generating every reported SoCPct. ValidatePlan instead replays from the unchanged p.InitialSoCPct, so the first action differs by the entire boundary violation and runReplan rejects every Go fallback plan, retaining an old or nil plan precisely while the battery needs recovery. Keep the actual energy for forward replay and restrict only the policy lookup index, or explicitly model a recovery action.
AGENTS.md reference: AGENTS.md:L19-L21
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f4cb411. Configure here.
Go fallback used to skip ValidatePlan, and forward simulation snapped SoC onto the DP grid, so reported energy did not replay. Python serialized full curtailment as pv_limit_w=0, the same sentinel as no cap, and applied the PV-charge bonus in every mode. Share BatteryEnergyDeltaWh, replay aggregate Go DP trajectories, start forward sim from real SoC, and add pv_curtail_active so a zero cap is a distinct contract. Gate the PV-charge bonus to passive_arbitrage. Leftover-PV and battery-to-EV residual constraints run per scenario. Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Policy lookup stays on the SoC grid. Forward simulation now integrates from the actual initial SoC, including starts outside the configured band, so ValidatePlan and the published trajectory match. An action that would worsen recovery is clipped to the remaining headroom instead of being replaced with idle or snapped after the fact. Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
4a91854 to
1f00ebe
Compare



Accepted text proposal
Issue or Discussion: #885 (replay / sentinel / engine alignment). Stacked on #957.
Maintainer comment that accepted this scope: plan-mode refactor of planning-model physics: one energy step, ValidatePlan on every published plan, distinct zero PV cap, PV-bonus mode gate.
What changed
loadpoint.BatteryEnergyDeltaWh. Forward simulation starts from real SoC instead of snapping onto the DP grid, so reported energy replays.runReplanrunsValidatePlanon Go fallback, not only on the Python optimizer. A trajectory that cannot be reconstructed is dropped; the previous plan stays.pv_curtail_active. Active withpv_limit_w = 0is a true zero cap. Inactive with 0 remains release. Python CVXPY, HiGHS, recourse, progressive and multistage all serialize through one helper.pv_charge_bonus_ore_kwhonly inpassive_arbitrage, matching Go DP.go/internal/loadpoint/testdata/site_physics.json.Why
Go fallback could publish a plan that would fail independent replay. Full PV curtailment was serialized as
pv_limit_w = 0, which Core treated as no cap and then rejected a physically valid solve. The same bonus setting changed the Python objective in modes where Go ignored it.Boundaries and safety
masterahead of that PR.dispatch.gois untouched (fix(control): mitigate PV-only export over limits #888 owns live PV export). A true zero cap can now pass ValidatePlan; hardware still does not get an active zero command.SlotDirectiveexecution is out of scope.Open PRs on overlapping files:
dispatch.goonly; not edited here.model.py,direct_highs.py,mpc.go; rebase when either lands.Verification
Local pre-commit ran
make verify. Local pre-push ranmake verify-all(including linux/arm64, linux/amd64, windows/amd64). Optimizer suite: 172 passed.No UI changed.
Checklist
Note
Medium Risk
Changes the plan-publish gate and shared energy/grid identities used by both Go DP and the Python optimizer. Dispatch hardware is untouched, but invalid or unreplayable trajectories are now dropped instead of going live.
Overview
Rejects any plan that cannot be reconstructed from the request before it becomes live, including Go DP fallback. Shared helpers (
BatteryEnergyDeltaWh,EffectivePVW) now drive both forward simulation andValidatePlan.Go DP integrates from the actual initial SoC (policy lookup stays on the grid) and clips actions that would worsen operating-band recovery. Aggregate DP trajectories without per-storage maps replay as one battery.
pv_curtail_activeseparates a true zero PV cap from “no cap”. Python solvers serialize through_pv_curtail_output. PV charge bonus applies only inpassive_arbitrage, matching Go. Battery-to-EV residual is constrained per scenario. Fuse-limit checks allow sub-watt solver residue only.A shared
site_physics.jsontable locks Go and Python identities together.Reviewed by Cursor Bugbot for commit 1f00ebe. Bugbot is set up for automated code reviews on this repo. Configure here.