Skip to content

bug: DP reward model treats surplus discharge as forced export, ignoring load_first self-throttling — causes avoidable grid import at SOE step boundaries #240

Description

@johanzander

Summary

While investigating a report on #126 (evening battery discharge shrank between two consecutive beta versions), found that the DP's reward-function flow-balance in _compute_reward always treats any battery discharge beyond home_consumption as exported to the grid — even for LOAD_SUPPORT and SOLAR_EXPORT periods, which map to Growatt load_first mode. Load_first hardware physically self-throttles its output to match actual demand (it doesn't blindly emit a fixed amount and dump the rest to the grid), so the model's assumption doesn't match reality for these intents.

Because the DP's SOE state grid is discretized at SOE_STEP_KWH = 0.1 kWh, the optimizer frequently can't land on the exact battery action that matches a period's consumption (e.g. consumption = 0.15 kWh, but only 0.1 or 0.2 kWh discharge steps are selectable). When choosing between the two, the reward model penalizes the larger step (0.2 kWh) as "0.05 kWh wasted on a low-margin export" — but in reality, because load_first self-throttles, the larger step would just be a rate ceiling: the inverter delivers exactly the 0.15 kWh needed, with zero import and zero export. The model has no way to represent "ceiling that self-adjusts to load," so it's biased toward under-selecting the discrete step, leaving real, avoidable grid import in the plan (and, since discharge_rate is action-derived and written straight to hardware, in the actual applied schedule too).

Evidence

Code — core/bess/dp_battery_algorithm.py:306-311:

energy_balance = (
    solar_production + battery_discharged - home_consumption - battery_charged
)
grid_imported = max(0, -energy_balance)
grid_exported = max(0, energy_balance)

This applies unconditionally regardless of strategic intent — there's no branch that distinguishes LOAD_SUPPORT/SOLAR_EXPORT (load_first, self-throttling) from BATTERY_EXPORT (grid_first, which genuinely does dump to the grid).

Contrast with core/bess/battery_system_manager.py:70-87 (solar_export_discharge_rate docstring), which correctly describes the real hardware behavior for load_first intents:

"SOLAR_EXPORT maps to load_first; the battery only discharges to cover an actual (sub-period) solar deficit."

Real debug export, bess-debug-2026-07-06-060026.md (v9.9.0b8, last optimization run at 06:00:09, period_data):

per 88 22:00 intent=LOAD_SUPPORT battery_action=-0.100 home_consumption=0.150 battery_to_home=0.100 battery_to_grid=0.000 grid_imported=0.050 buy=0.373 sell=0.141
per 92 23:00 intent=LOAD_SUPPORT battery_action=-0.100 home_consumption=0.135 battery_to_home=0.100 battery_to_grid=0.000 grid_imported=0.035 buy=0.361 sell=0.130

SOE at these periods was 13.6-14.1 kWh out of 15 kWh capacity, far above the 6.75 kWh minimum — this is not a capacity constraint. It's the 0.1 kWh SOE-grid resolution forcing a shortfall against 0.135-0.15 kWh of actual demand, when the battery had ample headroom to just cover it.

Practical consequence

Every period where consumption isn't an exact multiple of SOE_STEP_KWH (0.1 kWh) and the DP evaluates a load-following discharge (LOAD_SUPPORT, SOLAR_EXPORT), it may under-provision the discharge and leave an avoidable grid import — small per period (a few cents), but systematic across every evening/low-consumption period, every day. Users see this as "the battery isn't fully utilizing arbitrage" even though plenty of SOE remains.

Suggested investigation direction (not verified as sufficient — needs its own diagnosis)

  • For load_first/self-throttling intents (LOAD_SUPPORT, SOLAR_EXPORT), model battery_to_home = min(battery_discharged, home_consumption) with no forced export of the remainder, rather than the blanket "any surplus becomes export" rule.
  • Reserve the current "surplus is exported" logic for genuinely grid_first intents (BATTERY_EXPORT).
  • Re-validate whether this changes which discrete SOE step the backward-induction pass selects in low-consumption periods, and whether it removes small avoidable grid imports from predicted schedules.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-analysisDebug log present, awaiting @claude-bot analyze

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions