Audience: agents and developers changing the optimizer, intent classification, or inverter control mapping. For the human-facing overview, see
docs/DEVELOPMENT.md→ Testing.
The single most valuable verification tool in this codebase. Every savings
number the optimizer reports is a plan (P) — computed from the chosen battery
actions. But the inverter is driven by coarse modes (grid_first /
load_first / battery_first + charge/discharge rates), and a mode is a
policy, not a power setpoint. So the plan can claim things the hardware can't
deliver. Plan-only tests (run_scenario, scenario expected_results) cannot see
this gap.
The simulator (core/bess/simulation/) closes the loop: it takes the control
commands derived from a plan and computes the realized economics (R) of
executing them. The invariant is:
R == P— executing the optimizer's plan through the inverter must reproduce the planned economics (to within the DP's SoE/power-grid resolution). A larger gap is a real control-fidelity bug, not noise.
This is pure simulation (no hardware, no mock-HA), so it runs in unit tests.
from core.bess.simulation.inverter_simulator import (
derive_control_command, # plan period (intent + power [+ intra_period_discharge_allowed]) -> ControlCommand
simulate, # execute commands on conditions -> realized PeriodData + cost
)
from core.bess.simulation.verification import (
verify_plan_faithfulness, # optimize -> derive -> simulate -> (P, R, per-period deltas)
ab_compare, # realized-cost delta between two command sequences (exact)
realized_under_solar_error, # optimize on FORECAST solar, execute on ACTUAL solar (robustness)
)In scenario tests, use the helper run_scenario_realized(scenario) (in
tests/helpers.py) — it returns (result, realized_cost) so you can assert
R == P alongside the normal plan checks. test_scenarios.py does this for every
scenario.
If you touch the DP (dp_battery_algorithm.py), the intent classification
(strategic_intent.py), or the control mapping (inverter_controller.py),
you MUST verify R == P still holds — a passing plan-only test means nothing if
the plan isn't executable. Add/keep an R == P assertion for the affected
scenarios.
- Phantom solar-export over-crediting (#145): the optimizer booked export of
surplus a
load_first"store" period actually stores → ~8–16% inflated savings on sunny days. Invisible to plan-only tests. - Discharge can't be paced for home support (#147):
LOAD_SUPPORTdumps the battery greedily on deep evening peaks → realizes ~3–4% worse than planned. - Shadow-price discharge gate was invisible to the simulator (#388):
derive_control_commandhad no way to reach the sameintra_period_discharge_gateproduction uses for SOLAR_EXPORT/SOLAR_STORAGE (#187/#319) — anR == Ptest for gate-related changes was structurally unsatisfiable until this was fixed. Since #526 the parameter isintra_period_discharge_allowed: bool | None, not the oldshadow_price/buy_pricepair. The DP decides the gate where it owns the value function and records it onpd.decision.intra_period_discharge_allowed; pass that value through to exercise the gate.Nonemeans "this caller is not exercising the gate" and leaves it closed — deliberately distinct fromFalse, which is the DP deciding against opening it. Do not reintroduce a scalar here: ashadow_priceof0.0cannot be told apart from one that was never computed, which is the defect #526 removed. Production also applies this gate to LOAD_SUPPORT on TOU/register platforms (#384, reverted by #393, re-landed by #520) — the simulator deliberately does not mirror that, because a 15-min point-forecast simulator has no sub-period excess load to cover and so can only model the gate's cost, never its benefit. Seedocs/agents/bess-knowledge.md's SOLAR_EXPORT discharge gate section.
A real, unfixed R != P gap should be xfail'd with a tracking issue (so it
stays visible), never hidden under a loose tolerance. See the
DISCHARGE_PACING_SCENARIOS set in test_scenarios.py (→ #147). The grid-resolution
residual (max(0.5 SEK, 1% of |P|) in test_scenarios.py) is the only tolerance
— it reflects the DP's 0.1 kWh SoE grid, not a fudge factor.