Skip to content

feat: model grid connection import capacity in the DP optimizer - #433

Open
johanzander wants to merge 4 commits into
mainfrom
feat/issue-429-grid-import-cap
Open

feat: model grid connection import capacity in the DP optimizer#433
johanzander wants to merge 4 commits into
mainfrom
feat/issue-429-grid-import-cap

Conversation

@johanzander

Copy link
Copy Markdown
Owner

Summary

  • The DP had no ceiling on grid import: _ac_flows computed grid_imported = home_consumption - home_served with no cap, so the optimizer could plan to leave the battery idle and import an unbounded load through a fuse-limited connection — physically undeliverable, since HomePowerMonitor only throttles battery charging at runtime, never the household's own load.
  • Derives a per-period grid-import energy cap from the existing HomeSettings (voltage × max_fuse_current × safety_margin — deliberately not multiplied by phase_count, matching HomePowerMonitor's own worst-phase gating for unbalanced 3-phase loads), gated on power_monitoring_enabled.
  • Enforced as a hard constraint on total import (house load + battery grid-charging), not just the charging component. A period whose load alone exceeds the cap forces the battery to discharge and cover the excess (constrain, never raise); if even full discharge can't satisfy the cap, the DP falls back to the minimum-import action available rather than erroring.

Root cause

The DP has no grid import limit. _ac_flows returns grid_imported = home_consumption - home_served with no ceiling, so the optimizer is free to plan any import power in any period. The house service limit is configured (HomeSettings.max_fuse_current) but is enforced only at runtime by HomePowerMonitor, and only against battery charging. The optimizer never sees it.

Independently diagnosed and confirmed via bess-analyst against the current source (dp_battery_algorithm.py, power_monitor.py, settings.py) before implementation — see issue #429 for the full write-up and the 4 open design questions it raised.

Design decisions (per issue's open questions)

  1. Cap applies to total import, not just charging — the DP has both charge and discharge as levers; a charging-only cap would just reproduce today's runtime blind spot inside the plan.
  2. Constrain, never raise — matches the DP's existing convention for the AC-output cap and temperature derating (mask actions, don't except).
  3. Both the AC-output cap and the import cap apply independently, minimum-combined — they're different physical limits on opposite sides of the same AC stage.
  4. Hard-fail at settings-validation time (HomeSettings.__post_init__) if power_monitoring_enabled is set with implausible fuse/voltage/safety_margin values, never a silent no-op inside the DP.

Fix

Touches core DP internals since the cap couples the charge and discharge action spaces in a way the existing per-disposition ceilings (AC cap, temperature derating) don't need to: _state_transition, _state_transition_grid, _compute_reward (now returns (reward, new_cost_basis, grid_imported)), _compute_reward_grid (now returns (reward, grid_imported)), _build_period_data, _run_dynamic_programming (new feasibility mask: constrain to max(cap, minimum achievable import this period)), and _best_action_at_continuous_state (candidates gathered then filtered against the same floor logic for the continuous forward-reconstruction path).

A code-review pass caught and fixed one real bug before this PR opened: the cap formula initially multiplied by phase_count, but HomePowerMonitor gates on the single worst-loaded phase, not phase-summed power (a deliberate fix for unbalanced 3-phase loads, commit 37201cb9). Fixed, and the test scenario now uses phase_count=3 specifically to pin this.

docs/agents/bess-knowledge.md and docs/SOFTWARE_DESIGN.md updated with a new section on the cap's derivation and behavior.

Test plan

  • ./scripts/quality-check.sh passes locally (fast suite: 1378 passed, 15 skipped; frontend: 103 passed; lint/format clean)
  • .venv/bin/pytest -m slow passes (394 passed, 3 skipped)
  • New plan-faithfulness scenario test (core/bess/tests/unit/test_grid_import_cap.py) using run_scenario_realized — asserts R==P and that grid import stays within the fuse-derived cap during a load spike that would otherwise be economically preferable to import unconstrained (confirmed the pin discriminates: reverting the fix, or reintroducing the phase_count bug, makes the test fail)
  • Verified live through the real HTTP API: brought up the mock-HA/backend E2E stack, PATCH /api/settings to enable power_monitoring_enabled with a tight fuse limit, then ran the real optimize_battery_schedule inside the running container using the settings as persisted through the actual API — observed the DP discharge to satisfy the cap and correctly fall back to the physically-unavoidable minimum import once discharge capacity was exhausted

Closes #429

The DP had no ceiling on grid import: _ac_flows computed
grid_imported = home_consumption - home_served with no cap, so the
optimizer could plan to leave the battery idle and import an unbounded
load through a fuse-limited connection. HomePowerMonitor only throttles
battery charging at runtime, never the household's own load, so a plan
computed without this constraint could silently diverge from what the
fuse can actually deliver.

Derives a per-period grid-import energy cap from the existing
HomeSettings (voltage x max_fuse_current x safety_margin -- not
multiplied by phase_count, matching HomePowerMonitor's own worst-phase
gating for unbalanced 3-phase loads), gated on power_monitoring_enabled.
Enforced as a hard constraint on total import (load + battery
grid-charging), constraining rather than excluding a period whose load
alone exceeds the cap -- the battery is forced to discharge and cover
the excess, falling back to the minimum-import action available if even
full discharge can't satisfy it.

Touches core DP internals (_state_transition, _state_transition_grid,
_compute_reward, _compute_reward_grid, _build_period_data,
_run_dynamic_programming, _best_action_at_continuous_state) since the
cap couples the charge and discharge action spaces in a way the
existing AC-output cap and temperature derating (independent per-
disposition ceilings) don't need to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@johanzander
johanzander marked this pull request as ready for review July 30, 2026 19:44
Same root cause as #443/#436: branch was cut from a HEAD behind main
(before the v10.0.0 release cut), so it missed the CHANGELOG.md
condensation. Resolves the same way: #429's entry moves into a new
Unreleased section as a one-liner per the updated CHANGELOG guidance.
johanzander added a commit that referenced this pull request Aug 1, 2026
Same root cause as #443/#436/#433: branch was cut behind main, before
the v10.0.0 release cut, so it missed the CHANGELOG.md condensation.
Resolves the same way: #415's entry moves into a new Unreleased
section as a one-liner per the updated CHANGELOG guidance.
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.

Model grid connection import capacity in the optimizer

1 participant