Conversation
…hortfalls After the economic solve, a preference stage keeps that cost and then minimises the horizon import and export peaks. Hard site limits stay hard; a late or failed preference solve keeps the economic schedule. The solver payload names the stage, the resulting peaks, and any remaining flex/storage/thermal shortfall. Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
0.1 öre of cost slack was real money: flatten used it to cut profitable export and sneak a spread-blocked discharge. Bound the second solve to numerical noise, skip one-slot horizons, and reject a unique fuse-export nibble in tests. Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Flex and thermal already fell back to CVXPY, so their service report landed. A missed storage energy target on the direct HiGHS path did not. Name that shortfall in the same solver payload, and keep the economic schedule when the preference deadline has already expired. Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_8ba54eec-280a-4a11-90f6-70ab3df9b05b) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
miravoss26
left a comment
There was a problem hiding this comment.
What it does: after the economic MPC solve, runs a second cost-neutral "preference" solve that minimises the horizon's grid import/export peaks, and reports unmet EV/storage/thermal service in the solver payload so the UI can explain a missed deadline.
Correctness
- Cost-neutrality is enforced twice: the preference solve has the cost bound (
cost_objective <= cost_value + slack) baked in as a hard constraint, and both backends re-verify the actual spend againsteconomic_objective + slack + COST_BOUND_TOLERANCE_OREafter solving before accepting the candidate — good defense against a solver returning a technically-feasible-but-off-by-epsilon result. - Fuse limits stay hard because the preference problem reuses the same
constraintslist as the economic solve (HiGHS: same live model instance with rows added, not rebuilt) —test_flatten_does_not_cut_profitable_fuse_exportexplicitly proves the flatten stage can't push export pastmax_export_w, which is the claim in the changeset. - Every failure path (deadline exceeded, solver error, non-integral MIP solution, non-finite values, cost bound violated) falls back to the original economic incumbent (
STAGE_KEPT) rather than silently returning a worse or infeasible plan.test_expired_deadline_skips_flatten_and_keeps_peaksconfirms the preference solve doesn't even get invoked once the deadline's gone. - New
SolverInfo/ServiceReportfields areomitemptyon the Go side and only populated when computed on the Python side — genuinely dual-release, an older optimizer image omitting them doesn't break Core. - Test coverage is thorough and matches the changeset's claims point for point (cost-neutral, hard limits, deadline-safe, both HiGHS and cvxpy backends, EV/storage shortfall reporting).
Security: no secrets, no new deps, no new network destinations, no authz surface — pure solver/numerics change.
One blocker, unrelated to the code: gh pr view shows mergeable: CONFLICTING — this needs a rebase against main before it can land, independent of the review.
Nothing wrong with the logic from my read — safe to merge once rebased.
|
Closing under the owner-approved FTW direction. Core retired the Python optimizer in #1083, so this implementation will not be rebased or restored. Reporting unmet household goals and evaluating cost-neutral peak reduction remain useful requirements for the current Energyplan integration; the selected roadmap in #1214 records them. The PR and its commits remain available as reference. |
Inspired by evcc-io/optimizer: take the two things that are worth stealing, and leave the rest.
evcc’s planner is a cloud MILP that currently only visualises a schedule. Ours is already on the dispatch path. Their useful ideas are (1) a cost-neutral second solve that flattens grid peaks among equally priced schedules, and (2) telling the caller when a goal was missed. Soft grid limits, EV-as-battery, CBC/PuLP, and cloud-by-default stay out.
Accepted text proposal
This work was requested in the agent thread: compare evcc-io/optimizer with FTW, take the useful bits, and implement them. Maintainer (Fredrik) asked to take the comparison into a build plan and then implement.
What changed
After the economic solve, a preference stage keeps that cost (plus numerical slack, not 0.1 öre) and minimises the horizon’s import and export peaks. Site fuse limits stay hard. If the extra solve is late, fractional, or fails, the economic schedule is kept. Both the direct HiGHS champion and the CVXPY fallback do this. A one-slot horizon skips the second solve (
preference_stage: single_slot) because there is no tie to break across time.The solver payload now carries
preference_stage,import_peak_w,export_peak_w, and an optionalservice_reportwith remaining flex/storage/thermal shortfall. The HiGHS champion path reports storage-target shortfall; flex and thermal still go through CVXPY (direct HiGHS is ineligible for those assets). Core unmarshals those ontoSolverInfoso/apialready exposes them onactive_solver. No UI copy in this PR.settings.flatten_peaksdefaults to true.falseturns the stage off. Handshake feature:preference_flatten_peaks. Protocol version is unchanged.Why this implementation
Putting a tiny peak weight in the same objective is what evcc had to abandon: the MIP gap swallows it. They freeze money, then break the tie. Same here, as a third stage after the existing service-then-economic split. That is the right place: service stays lexicographic, money stays lexicographic, peak shape is only a tie-break.
0.1 öre of cost slack was real money: at 50 öre/kWh it buys 2 W of peak reduction. That failed unique-export and arbitrage-spread tests. Slack is now 1e-8 öre (or 1e-12 of |cost|), which is solver noise, not a watt budget.
Boundaries and safety
model.py. This PR’smodel.pydelta is the post-economic hook only; rebase onto fix(mpc): surplus-only EV can take leftover PV while the home battery grid-charges #957 if they land together. Both also touchgo/internal/mpc/mpc.go/external_optimizer_test.go. fix(mpc): surplus-only EV can take leftover PV while the home battery grid-charges #957 has no reviews yet.Dual release
Old optimizer images omit the new fields; Core ignores unknown JSON today and understands the fields after this. Flattening only happens once the optimizer image includes this commit. Ship a new
ftw-optimizerimage with this Core.Verification
optimizer/.venv/bin/pytest -q optimizer/tests go test ./internal/mpc -count=1flatten_peaks: false(abs 1e-3 öre)single_slotand keep the economic planno_timeand does not start the second solveChecklist
model.py; this hook is post-economic only.