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
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_rewardalways treats any battery discharge beyondhome_consumptionas exported to the grid — even forLOAD_SUPPORTandSOLAR_EXPORTperiods, which map to Growattload_firstmode. 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.1kWh, 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:This applies unconditionally regardless of strategic intent — there's no branch that distinguishes
LOAD_SUPPORT/SOLAR_EXPORT(load_first, self-throttling) fromBATTERY_EXPORT(grid_first, which genuinely does dump to the grid).Contrast with
core/bess/battery_system_manager.py:70-87(solar_export_discharge_ratedocstring), which correctly describes the real hardware behavior for load_first intents:Real debug export,
bess-debug-2026-07-06-060026.md(v9.9.0b8, last optimization run at 06:00:09,period_data):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)
LOAD_SUPPORT,SOLAR_EXPORT), modelbattery_to_home = min(battery_discharged, home_consumption)with no forced export of the remainder, rather than the blanket "any surplus becomes export" rule.BATTERY_EXPORT).Related