docs: normative optimizer target architecture + migration plan - #514
Conversation
…ion plan Synthesizes the 2026-08-08 architecture assessment, the PWL-everywhere benchmark (#512/#513), and ridax67's #466 IDLE analysis into: - docs/agents/optimizer-architecture.md — normative principles all future optimizer changes must uphold or amend: one selector, one lexicographic tie-policy table, executable-command candidates, single-sourced flows, one epsilon, certified exactness, structural (not stochastic) risk handling. Indexed in CLAUDE.md. - docs/superpowers/plans/2026-08-09-optimizer-target-architecture.md — Phase 0 (drain WIP) through Phase 4 (command-space candidates), with bit-parity gates, acceptance criteria mapped to open issues (#320/#352/#393/#466/#485/#487/#497/#512/#513), and parallel tracks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
johanzander
left a comment
There was a problem hiding this comment.
Thorough review of the proposed architecture (claims fact-checked against main, tie-policy economics stress-tested via bess-analyst).
Verdict: the architecture is sound and worth adopting — the three defect classes are real, the issue lineage checks out against the code, and the compliance checklist is enforceable rather than aspirational. But two findings should be fixed in this PR since the document is normative from the moment it lands:
- Row 2 silently narrows the #466 guard from "non-idle winner" to "discharge winner" — an undeclared third semantic change (inline comment).
- P6 is aspirational but reads as descriptive — today's
splice_schedulerelies on PWL exactness unconditionally, which is exactly what P6 forbids, and nothing polices it (inline comment).
Plus three smaller fixes (P1 wording vs the Phase 1 vectorized escape hatch, two cited artifacts that don't exist on main yet, and a wrong "branch for branch" citation) and a few docstring-level notes — all inline.
What checked out clean: all named symbols exist where claimed; the four tie-break call sites and the epsilon <= 0 early-return; the terminal-value method split (battery_system_manager.py:1920, #422 day-scoping in the caller at :2124); the PLAN_EXECUTION_GAP_SEK +0.73/+0.56/+0.45 pins; the #350 fold still in EnergyData._calculate_detailed_flows (models.py:157); no stray hardcoded epsilons contradicting P5; row 1 subsumes (not contradicts) the #510 tie-break — _prefer_curtailed_charge_absorb contains the identical grid-import guard internally; and P7's structural-over-stochastic stance is economically bounded (worst observed forfeiture +0.032 SEK vs the 0.05 SEK/fixture budget).
| not — no preference reorders a discharge winner; #466/#510 ordering | ||
| made structural). | ||
| 3. **Prefer** the largest load-tracking discharge ≤ net load (+half a | ||
| rate step, capped at `BATTERY_EXPORT_THRESHOLD_KWH/dt`) within epsilon |
There was a problem hiding this comment.
Should-fix: row 2 silently narrows the #466 guard from "non-idle winner" to "discharge winner".
Today both _prefer_* functions bail on any non-idle argmax winner — including a charge winner (_prefer_load_covering_discharge bails at dp_battery_algorithm.py:1430 on abs(best_power) > POWER_TOLERANCE_KW; _prefer_curtailed_charge_absorb at :1494). Row 2 as written protects only discharge winners, so under the new table a within-epsilon charge winner during positive net load falls through to row 3 and can be flipped to a load-covering discharge — a charge→discharge action flip that passes the row-1 guard (discharge imports less grid) and is not one of the plan's "two deliberate semantic changes".
Either extend row 2 to "non-idle winner" (true parity with today's ordering), or declare this as the third deliberate change with its own acceptance test.
| explicit error bound. #513 (PWL mis-ranks a plan by 0.584 SEK while | ||
| "exact") is the standing counterexample: until fixed, PWL splices are | ||
| treated as *heuristic* improvements, and any new reliance on their | ||
| exactness is forbidden. |
There was a problem hiding this comment.
Should-fix: P6 is aspirational but reads as descriptive.
splice_schedule (dp_battery_algorithm.py:2290) splices unconditionally — there is no compare-against-grid-segment, accept-only-if-better gate. The only guard is the exactness certification raise (PWLWindowUnderRefinedError), and #513 is the proof that certification doesn't hold. So the existing reliance on PWL exactness is exactly what P6 forbids, and nothing polices it — "treated as heuristic" is currently a doc claim, not a property of the code.
Cheap fix that makes P6 true: accept a spliced window only if its replayed cost is ≤ the grid segment's (a _replay_accounting_pass comparison). Suggest adding that as a P6 compliance item or a small Phase 1/2 rider.
| There is exactly **one** implementation of candidate enumeration + | ||
| evaluation + selection, parameterized by a continuation-value evaluator | ||
| `eval_V(next_soe) -> float`. The grid backward induction, the grid forward | ||
| replay, the PWL backward induction, and the PWL forward replay all call it. |
There was a problem hiding this comment.
Should-fix: P1's "exactly one implementation" collides with Phase 1's own escape hatch.
The backward induction (_run_dynamic_programming:949) is fully numpy-vectorized — on the order of tens of thousands of candidates per period — and the migration plan sensibly allows keeping the vectorized fast path if bit-parity holds. But that outcome (vectorized path + select_action sharing enumeration functions, pinned by a parity test) violates P1 as literally worded, forcing a doc amendment during Phase 1 itself.
Suggest rewording P1 to what's actually meant: one candidate-enumeration + one tie policy, with any vectorized evaluator parity-pinned against the selector.
| hand-mirrored near-clones (grid replay `_best_action_at_continuous_state` | ||
| vs `_pwl_best_action_at_continuous_state`) are the #236-class / | ||
| `DISCHARGE_LATTICE_PCT_EPS`-class bug factory; the mirror comments | ||
| ("branch for branch") are the symptom. |
There was a problem hiding this comment.
Should-fix: wrong citation. The "branch for branch" mirror comment is at dp_battery_algorithm.py:447, on the _compute_reward/_compute_reward_grid pair — which this doc's own "What this document does NOT change" section explicitly blesses. The duplication P1 actually targets (_best_action_at_continuous_state 213 lines vs the PWL clone ~221 lines) has "mirroring" comments (pwl_window_dp.py:164,442,458,912) but not that phrase — a reviewer grepping the quote lands on the protected pair.
Also worth tightening: the _prefer_*/_discharge_candidates/_charge_candidate/_tie_margin helpers are not hand-cloned in PWL — pwl_window_dp.py:21-28 imports and calls them directly. Only the two enumerate/select functions are true clones. That makes P1's claim more precise, not weaker.
| explicit exact-vs-ingested decision in the plan). | ||
| - [ ] Acceptance (already written, this is the point): the #497 pin in | ||
| `test_flow_coherence.py` — 182/1875 incoherent periods — goes to | ||
| **0**, and the plan-execution gap pins that the fold caused |
There was a problem hiding this comment.
Should-fix: cited artifact doesn't exist on main. test_flow_coherence.py (and its 182/1875 pin) is not on main — it lives in an unmerged worktree. Since it's Phase 3's acceptance anchor, either add landing it to Phase 0's drain list or mark it "pending WIP" here. Same for scripts/bench_pwl_everywhere.py (the #512 gate, referenced at line ~345) — also not on main yet.
|
|
||
| 1. **Guard:** candidates importing more grid than the argmax winner are | ||
| ineligible (+1e-9 tolerance). | ||
| 2. **Guard:** if the argmax winner is a discharge, return it (decisive or |
There was a problem hiding this comment.
Docstring note (no change needed): row 1 is consistent with today — _prefer_curtailed_charge_absorb contains the identical grid-import guard internally (dp_battery_algorithm.py:1504), so row 1 subsumes rather than contradicts the #510 rule; the #510 winner holds SOE with ~zero import, and the only candidate blocked is the grid top-up, blocked today too.
The one foreclosure worth recording in row 1's rationale: in negative buy-price windows (exactly when curtailment floors bite), a within-epsilon grid top-up is arguably the safer pick against a solar shortfall — and row 1 permanently bans it. Codifies status quo, so fine, but a future amendment will want this context.
| 2026-08-07-232503 bundle fixture, the 06:00–06:59 crossover period must | ||
| resolve to a tracking mode, not IDLE, whenever a lattice-feasible | ||
| discharge ≤ net load exists; where none exists (86 W load vs 200 W | ||
| minimum gear) IDLE legitimately stands and the test pins that too. |
There was a problem hiding this comment.
Note: the acceptance hinges on whether the 06:00 crossover is a bit-exact tie. Dropping the epsilon <= 0 early-return is economically sound — cycle cost is already inside _compute_reward, so on an exact tie tracking is weakly dominant under forecast error (IDLE imports the whole consumption miss at buy price; tracking absorbs it at ~breakeven). But at epsilon 0, "within epsilon" catches only bit-exact ties. If the ridax67 crossover period actually has a tiny nonzero gap, the test can only pass by introducing an epsilon floor — and a floor would trade real model value and needs its own stated bound in the row docstring. Worth checking the bundle before writing the RED test.
| battery_solar_cost, stored as a generated JSON under | ||
| `tests/unit/data/golden/` in the first commit) and assert the | ||
| refactored path reproduces them bit-identically. | ||
| - [ ] Run it against unrefactored code to prove the golden capture is |
There was a problem hiding this comment.
Note: state the golden-fixture lifecycle across phases. Phase 2 deliberately changes behavior, which breaks Phase 1's bit-parity goldens by design. Presumably the answer is "regenerate goldens as part of Phase 2's measured-delta step" — say so explicitly, so nobody "fixes" the parity test by deleting it.
| confusing. | ||
|
|
||
| - [ ] Merge PR #510 (approved 2026-08-08; two optional test nits may ride | ||
| along or follow up). |
There was a problem hiding this comment.
Already done — #510 merged 2026-08-08 (742d5906, current main HEAD). Can be checked off.
…ing, citations - Row 2 guards any non-idle winner (charge or discharge), exact parity with both _prefer_* bail conditions; discharge-only narrowing was an undeclared semantic change. - P6 rewritten as explicitly aspirational with the Phase 2 splice cost-gate rider that makes it a code property; rider added to Phase 2 with RED test on the #513 shape. - P1 reworded: one enumeration + one tie policy; vectorized backward evaluator allowed if parity-pinned; correct clone citations (the enumerate/select pair), 'branch for branch' belongs to the protected reward pair. - Phase 0: #510 checked off (742d590); land test_flow_coherence.py and bench_pwl_everywhere.py (neither on main yet). - Row 1: negative-buy-price foreclosure recorded; crossover acceptance now requires measuring the bundle margin first (epsilon floor needs a stated bound); golden-fixture lifecycle stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All five should-fixes and the notes are in (single commit):
Notes folded in: row 1 records the negative-buy-price foreclosure for future amendments; Phase 2's crossover acceptance now starts by measuring the bundle's actual margin (an epsilon floor, if needed, requires a stated forfeiture bound inside the 0.05 budget); golden lifecycle stated — Phase 2+ regenerates goldens in the measured-delta step, the parity test is never deleted. |
Summary
docs/agents/optimizer-architecture.md— normative: every PR touching the optimizer core must uphold principles P1–P7 or amend the document in the same PR. Codifies the 2026-08-08 architecture assessment (ten correction layers, three defect classes), the PWL-everywhere benchmark results (Grid DP discretization leaves 0.01-0.36 SEK/day unrealized on 20/22 benchmark fixtures #512, PWL window solver mis-ranks plans: picks 0.584 SEK worse schedule than grid DP under its own objective #513), and ridax67's Question: How is IDLE used? #466 principle ("IDLE must express that the optimizer WANTS to hold, never that it EXPECTS balance") as the intent-semantics rule.Notes
🤖 Generated with Claude Code