Summary
While investigating #231 (whether the whole-day profitability gate is still needed), found that the per-action anti-cycling floor in _compute_reward — the mechanism intended to block individually-unprofitable discharges during backward induction — is fed a frozen initial_cost_basis for every period after the first, not the true, path-dependent FIFO cost basis. The side-channel meant to thread cost basis forward through the DP is dead code due to a loop-order bug, and its return value is discarded by the caller anyway.
Evidence
core/bess/dp_battery_algorithm.py, _run_dynamic_programming (lines 659-889):
C = np.full((horizon + 1, len(soe_levels)), initial_cost_basis) # line 699
for t in reversed(range(horizon)): # line 705 — descends horizon-1 -> 0
for i, soe in enumerate(soe_levels):
...
reward, new_cost_basis = _compute_reward(
...,
cost_basis=C[t, i], # line 769 — read for period t
)
...
if best_action != 0 and t + 1 < horizon:
...
C[t + 1, next_i] = best_new_cost_basis # line 889 — write to period t+1
Because the outer loop counts down from horizon-1 to 0, period t+1 is fully processed in an earlier loop iteration than period t. The write at line 889 (C[t+1, ...]) targets a slot that has already been read and consumed by the time it's written — it can never be read back by anything in this function. C[t, i] is therefore just the constant initial_cost_basis for every (t, i) evaluated during the entire backward pass, never a path-dependent value.
Confirmed the return value is also discarded by the sole caller, optimize_battery_schedule (line 1100):
V, policy, _, _ = _run_dynamic_programming(...)
A second, correctly forward-threaded cost basis is computed afterward in a separate replay loop (lines 1129-1177, ascending for t in range(horizon)), which is what feeds the reported cost_basis in period data. But by then the action at each period was already locked in by the (cost-basis-blind) backward pass. If replaying with the true cost basis would make the already-chosen discharge fail the anti-cycling floor, the code explicitly does not reject or reconsider it — it just keeps the current basis and executes the discharge anyway:
# lines 1159-1162
# _compute_reward returns (-inf, cost_basis) for a blocked discharge; when
# replaying the already-chosen policy action just keep the current basis.
if reward == float("-inf"):
new_cost_basis = current_cost_basis
Practical consequence
battery_wear_cost = 0.0 for discharge rewards (line 359), so a discharge's per-period dollar reward has no wear cost baked in during the backward pass — the anti-cycling floor (comparing against cost basis) is the only thing preventing an opportunistic discharge. That floor is correct for a run's very first decision (where cost_basis genuinely equals initial_cost_basis), but for any later period in a multi-cycle schedule — charge now, discharge later at a different price — it's evaluated against a stale basis, not the true one the executed path would accumulate. The existing regression suite already builds and expects multi-window schedules with an initial_cost_basis distinct from later charge/discharge windows as the normal case (core/bess/tests/unit/test_optimization_algorithm.py:335-406), not an edge case — so this isn't a narrow corner condition.
Why this matters for #231
#231's fix (narrowly scoped) corrects the whole-day profitability gate's baseline (solar_only_cost). That gate remains a legitimate backstop specifically because this per-action floor can't be trusted beyond period 0 of a multi-cycle run. This issue is the deeper reason a whole-horizon check is still needed at all, and is being filed separately given its larger scope: any fix here likely needs cost_basis folded into the DP's state space (or an equivalent correct threading), and will very likely require regenerating several pinned test fixtures that currently encode the buggy economics (per the precedent in #209's PR, which had the same kind of fallout when a related reward-function bug was fixed).
Suggested investigation direction (not verified as sufficient — needs its own diagnosis)
- Quantify how often this actually changes which action is chosen in practice (vs. being inert because most single re-optimization runs don't involve enough distinct charge/discharge price windows for the frozen basis to matter).
- Consider whether cost basis needs to become part of the DP's state (bigger, more expensive state space) or whether a cheaper approximation (e.g., re-running backward induction with cost basis from a first forward pass) is sufficient.
Related
Summary
While investigating #231 (whether the whole-day profitability gate is still needed), found that the per-action anti-cycling floor in
_compute_reward— the mechanism intended to block individually-unprofitable discharges during backward induction — is fed a frozeninitial_cost_basisfor every period after the first, not the true, path-dependent FIFO cost basis. The side-channel meant to thread cost basis forward through the DP is dead code due to a loop-order bug, and its return value is discarded by the caller anyway.Evidence
core/bess/dp_battery_algorithm.py,_run_dynamic_programming(lines 659-889):Because the outer loop counts down from
horizon-1to0, periodt+1is fully processed in an earlier loop iteration than periodt. The write at line 889 (C[t+1, ...]) targets a slot that has already been read and consumed by the time it's written — it can never be read back by anything in this function.C[t, i]is therefore just the constantinitial_cost_basisfor every(t, i)evaluated during the entire backward pass, never a path-dependent value.Confirmed the return value is also discarded by the sole caller,
optimize_battery_schedule(line 1100):A second, correctly forward-threaded cost basis is computed afterward in a separate replay loop (lines 1129-1177, ascending
for t in range(horizon)), which is what feeds the reportedcost_basisin period data. But by then the action at each period was already locked in by the (cost-basis-blind) backward pass. If replaying with the true cost basis would make the already-chosen discharge fail the anti-cycling floor, the code explicitly does not reject or reconsider it — it just keeps the current basis and executes the discharge anyway:Practical consequence
battery_wear_cost = 0.0for discharge rewards (line 359), so a discharge's per-period dollar reward has no wear cost baked in during the backward pass — the anti-cycling floor (comparing against cost basis) is the only thing preventing an opportunistic discharge. That floor is correct for a run's very first decision (wherecost_basisgenuinely equalsinitial_cost_basis), but for any later period in a multi-cycle schedule — charge now, discharge later at a different price — it's evaluated against a stale basis, not the true one the executed path would accumulate. The existing regression suite already builds and expects multi-window schedules with aninitial_cost_basisdistinct from later charge/discharge windows as the normal case (core/bess/tests/unit/test_optimization_algorithm.py:335-406), not an edge case — so this isn't a narrow corner condition.Why this matters for #231
#231's fix (narrowly scoped) corrects the whole-day profitability gate's baseline (
solar_only_cost). That gate remains a legitimate backstop specifically because this per-action floor can't be trusted beyond period 0 of a multi-cycle run. This issue is the deeper reason a whole-horizon check is still needed at all, and is being filed separately given its larger scope: any fix here likely needscost_basisfolded into the DP's state space (or an equivalent correct threading), and will very likely require regenerating several pinned test fixtures that currently encode the buggy economics (per the precedent in #209's PR, which had the same kind of fallout when a related reward-function bug was fixed).Suggested investigation direction (not verified as sufficient — needs its own diagnosis)
Related
bess-analystinvestigation + independent verification against source, 2026-07-04).