Skip to content

fix(optimizer): keep SoC-above-max plans replay-consistent - #773

Merged
frahlg merged 4 commits into
masterfrom
agent/fix-optimizer-soc-above-max
Aug 4, 2026
Merged

fix(optimizer): keep SoC-above-max plans replay-consistent#773
frahlg merged 4 commits into
masterfrom
agent/fix-optimizer-soc-above-max

Conversation

@frahlg

@frahlg frahlg commented Aug 4, 2026

Copy link
Copy Markdown
Member

What changed

Canonicalize storage state once before any shared, recourse, or multistage model, scenario, or service-level builder runs. Solver-scale initial energy above the operating maximum is replaced with the exact maximum value within the documented 1e-6 Wh physical-input tolerance plus float representation error. An internal guard marker preserves the original over-bound signal across repeated normalization, while material over-band starts still use the mutually exclusive charge/discharge formulation.

The canonical storage specs are used for model parameters, multistage shared constraints, service-level solves, direct-HiGHS/CVXPY fallback paths, response construction, cache keys, and replay validation. Core validation and its tolerances are unchanged. Replay validation still rejects simultaneous cycling that causes an energy mismatch and forces the guarded retry.

Regression coverage

  • Multistage auto and cvxpy with cheap-charge, two 60-minute zero-load/zero-PV slots, and initial deltas 0.1e-6, 0.25e-6, 0.5e-6, 0.99e-6, and 1e-6 Wh.
  • The full boundary grid repeated four times per backend.
  • The full grid repeated under an active CPU-load thread.
  • Material over-band starts at 9800 Wh.
  • Shared, recourse, and multistage replay checks for SoC above maximum.
  • Auto fallback after direct-HiGHS detects simultaneous cycling.
  • The isolated optimizer suite on the rebased head: 66 passed.

Verification

  • Rebased on master 8d39cd91c82095948663cbf4e3114da8f299e11b after build: run one base across the whole stack (debian:trixie-slim) #731.
  • No open PR overlaps Dockerfile.optimizer; it is present only in the master base, not in this diff.
  • The final local make verify-all passed, including Linux arm64, Linux amd64, and Windows amd64 cross-compiles.
  • Fresh CI for head d4ca6084f8aa140bc0b1913e6b46cb827abad0ca is green: test run 30897784909, repo hygiene 30897784857, changeset-check 30897784808, and brand-cleanup 30897784955. The test run passed optimizer (Python + contract) and Go test + vet; unrelated jobs were path-skipped.
  • Final head: d4ca6084f8aa140bc0b1913e6b46cb827abad0ca.

Closes #768

The independent review reported no P0/P1/P2 findings. PR is ready for review and merge. Core validation was not weakened.

@frahlg
frahlg force-pushed the agent/fix-optimizer-soc-above-max branch 6 times, most recently from ed427e1 to 082717c Compare August 4, 2026 09:25
@frahlg
frahlg force-pushed the agent/fix-optimizer-soc-above-max branch from 082717c to d4ca608 Compare August 4, 2026 09:46
@frahlg
frahlg marked this pull request as ready for review August 4, 2026 09:53
@frahlg
frahlg merged commit bc2ed86 into master Aug 4, 2026
13 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4ca6084f8

ℹ️ 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".

mip_gap = float(value)
break
solve_ms = (time.perf_counter() - started) * 1000.0
_validate_storage_replay(actions, slots, [storage.spec for storage in storages])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry replay failures in shared and recourse solvers

When formulation: relaxed encounters negative-price slots, the continuous shared model can simultaneously charge and discharge after reaching the maximum SoC, so this newly added validation raises ReplayConsistencyError; worker.handle then returns internal_error instead of an optimizer plan. This is reproducible in cheap_charge mode even from an in-bound initial SoC, and the analogous validation in recourse.py fails identically. Unlike the multistage path, neither solver retries with mutually exclusive charge/discharge constraints, so both should apply the same guarded retry before returning an error.

Useful? React with 👍 / 👎.

float(spec["capacity_wh"]),
float(spec.get("min_energy_wh", 0)),
float(spec.get("max_energy_wh", spec["capacity_wh"])),
float(spec["initial_energy_wh"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep changing SoC out of the DPP cache key

Including initial_energy_wh in the model-shape key makes every normal replanning tick miss _MODEL_CACHE, because measured battery energy changes between requests and the one-entry cache immediately evicts the previous compiled model. The initial energy is already represented by CompiledMultistage.initial_energy and updated in assign(), so it does not affect model shape; this addition defeats the DPP cache and forces an expensive CVXPY rebuild on every changing-SoC solve.

Useful? React with 👍 / 👎.

@frahlg
frahlg deleted the agent/fix-optimizer-soc-above-max branch August 7, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimizer burns stored energy via simultaneous charge+discharge when SoC starts above soc_max — every plan rejected until the battery drains

1 participant