Skip to content

fix(plan): skip low power charging when the charge window overlaps solar - #4373

Merged
springfall2008 merged 1 commit into
mainfrom
fix/low-power-charging-pv-overlap
Jul 29, 2026
Merged

fix(plan): skip low power charging when the charge window overlaps solar#4373
springfall2008 merged 1 commit into
mainfrom
fix/low-power-charging-pv-overlap

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Problem

In low power charging mode, a charge window that overlaps solar production can make the plan more expensive.

The planner costs every charge window at the full charge rate — low power is only applied to the final plan (prediction.py gates it on save in ["best", "best10", "test"]). find_charge_rate then sizes the throttled rate purely from charge_left / minutes_left, with no knowledge of PV.

In a window overlapping solar the rate cap in the charge branch

battery_draw = -max(min(charge_rate_now_curve_step, max(charge_limit_n - soc, pv_now)), 0, -battery_to_max)

stops the PV reaching the battery. The surplus is exported at the export rate and the charge target is then made up from grid import once the sun has gone — costing more than the full rate charge the planner had already committed to.

Fix

find_charge_rate returns the max rate when the PV forecast summed across the remainder of the charge window exceeds LOW_POWER_PV_THRESHOLD (0.1 kWh). The test is window-wide rather than per-step because the throttled rate is chosen to span the whole remaining window, not the current instant.

Both callers supply that sum:

  • prediction.py — a suffix sum of the PV step array, built once per run and only when low power is active, so the optimiser's hot loop is untouched.
  • execute.pypv_forecast_minute summed between now and the window end.

No C++ kernel change is needed: the kernel only runs when save is falsy, which is exactly when charge low power is off, so it already takes the max rate path (prediction_kernel.cpp:708) and stays in parity. Parity revisions deliberately not bumped.

Tests

All written first and watched fail:

Test Without the fix
low_power_pv_* (model) ERROR: Metric 0.44 should be 0.0 — 44p worse than the 0p full rate plan
charge_low_power_pv (execute) Charge rate should be 2000 got 600
find_charge_rate_pv (unit) TypeError: unexpected keyword argument
  • Model — 6kW of PV for 2 hours, 8-hour window to 12kWh. Full rate lets the PV alone fill the battery for 0p; throttled to 1.5kW it exports 9kWh at 5p and re-imports it at 10p. low_power_dark_* confirms low power still applies with no sun.
  • Executecharge_low_power_pv (1kWh forecast in the window → max rate) and charge_low_power_pv_trace (0.05kWh, under threshold → keeps the throttled 600W).
  • Unitfind_charge_rate either side of the threshold.

Harness additions: pv_hours on simple_scenario (it only did constant PV) and pv_forecast on run_execute_test, which rebuilds pv_forecast_minute per scenario so a solar run cannot leak into the next one.

Full suite passes (110.72s), pre-commit clean, docs updated.

Trade-off worth noting

The test covers the remaining window, so a window starting in darkness and ending in sun now charges at full rate from the outset rather than deferring into the solar period. That is deliberate — it reproduces what the planner costed — but it does give up low-power operation on the dark portion of such windows.

🤖 Generated with Claude Code

The planner costs every charge window at the full charge rate - low power
charging is only applied to the final plan (prediction.py gates it on
save in best/best10/test). find_charge_rate then sizes the throttled rate
from charge_left / minutes_left with no knowledge of PV.

In a charge window that overlaps solar production the rate cap in

    battery_draw = -max(min(charge_rate_now_curve_step, ...), 0, -battery_to_max)

stops the PV reaching the battery. The surplus is exported at the export
rate and the charge target is then made up from grid import once the sun
has gone, costing more than the full rate charge that was planned for.

find_charge_rate now returns the max rate when the PV forecast summed
across the remainder of the charge window exceeds LOW_POWER_PV_THRESHOLD
(0.1kWh). Both callers supply that sum: the prediction from a suffix sum
of the PV step array built once per run (only when low power is active,
so the optimiser hot loop is untouched), and execute from
pv_forecast_minute between now and the window end.

No C++ kernel change is needed - the kernel only runs when save is falsy,
which is exactly when charge low power is off, so it already takes the max
rate path and stays in parity.

Tests: model scenarios low_power_pv_* / low_power_dark_* (without the fix
the low power run costs 44p against a 0p full rate plan), execute
scenarios charge_low_power_pv / charge_low_power_pv_trace, and a
find_charge_rate unit test for the threshold either side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents “low power charging” from being applied in charge windows that overlap forecast PV production, avoiding cases where throttling inadvertently forces PV export and later grid re-import (making the plan more expensive than the planner’s full-rate costing).

Changes:

  • Add a PV-overlap guard to find_charge_rate to default to max charge rate when forecast PV within the remaining window exceeds LOW_POWER_PV_THRESHOLD.
  • Plumb “PV remaining in window (kWh)” into find_charge_rate from both the prediction path (via a suffix sum) and the execution path (via summing minute PV forecast).
  • Add model, execute, and unit tests plus small test-harness extensions to cover PV-overlap behavior and prevent PV forecast leakage between execute scenarios.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
docs/customisation.md Documents that low-power charging is skipped when PV overlaps a charge window, with rationale.
apps/predbat/const.py Introduces LOW_POWER_PV_THRESHOLD (kWh) constant for the PV-overlap cutoff.
apps/predbat/utils.py Extends find_charge_rate with pv_window_kwh and enforces max-rate charging when PV overlap exceeds threshold.
apps/predbat/prediction.py Computes PV suffix sums when low-power is active and passes window PV overlap to find_charge_rate.
apps/predbat/execute.py Sums PV forecast over the active window remainder and passes the overlap energy to find_charge_rate.
apps/predbat/unit_test.py Registers the new find_charge_rate_pv unit test.
apps/predbat/tests/test_infra.py Adds pv_hours support to constrain PV duration in simple_scenario for targeted model tests.
apps/predbat/tests/test_model.py Adds model tests proving low-power does not increase cost with PV overlap, and still applies in dark windows.
apps/predbat/tests/test_find_charge_rate.py Adds a focused unit test for PV-threshold behavior in find_charge_rate.
apps/predbat/tests/test_execute.py Adds execute tests for PV overlap (above/below threshold) and resets PV forecast per scenario to avoid leakage.

@springfall2008
springfall2008 merged commit 4dc5cff into main Jul 29, 2026
3 checks passed
@springfall2008
springfall2008 deleted the fix/low-power-charging-pv-overlap branch July 29, 2026 18:23
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.

2 participants