Skip to content

candidate generation

github-actions edited this page Jun 11, 2026 · 3 revisions

HSEM Candidate Generation — Deep Dive

This document explains how candidate plans are generated, how assumptions drive the candidate set, and the mathematical models behind each strategy.


Table of Contents

  1. Why multiple candidates?
  2. Candidate list
  3. Assumptions behind each candidate
  4. MILP global optimisation
  5. Hysteresis

Why multiple candidates?

Battery scheduling is a sequential decision problem under uncertainty (prices, PV, and load are forecasts, not certainties). A single heuristic strategy may miss the global optimum under certain market conditions. By evaluating multiple independent strategies and picking the cheapest valid one, the planner:

  • Captures more of the available arbitrage value
  • Degrades gracefully when forecasts are wrong
  • Provides explainable alternative plans for debugging

Candidate list

The generator (planner/candidate_generator.py) operates in MILP-only mode. The MILP finds the globally optimal solution; heuristic candidates are disabled because the MILP consistently dominates them. Only diagnostic baselines remain alongside the MILP:

# Name Strategy
1 no_action Battery completely idle — no forced charge or discharge
2 passive Solar charging only where PV surplus exists; no grid charge or forced discharge
3 milp Globally-optimal LP solution (when scipy is available)

Historical candidates (disabled)

The following candidates were previously generated but are now commented out in MILP-only mode. They remain documented for reference and may be re-enabled as diagnostic tools:

Name Strategy
baseline Current HSEM scheduling output (discharge → charge → excess export → optimisation)
grid_charge Grid-charge slots kept; solar charging removed
solar_only Only solar-charge slots kept; grid charging cleared
discharge_only Discharge slots kept; all charge slots cleared
aggressive Cheapest N slots forced to grid-charge; most expensive M slots forced to discharge
soc_plan_25/50/75/100/125/full Partial-SoC candidates charging different fractions of discharge-window need

Assumptions behind each candidate

no_action (diagnostic floor)

Assumption: The inverter can be left in its default self-consumption mode (no external scheduling). PV surplus is exported; battery only moves according to its native operating logic.

Purpose: Provides a baseline cost that all other candidates must beat. If no candidate beats no_action, the planner falls back to doing nothing.

Mathematical model:

  • All recommendations cleared to None
  • No grid charge, no forced discharge, no force export
  • SoC simulation still runs: PV charges battery if native logic would do so
  • Terminal SoC is still accounted for

passive (inverter default)

Assumption: The inverter's default behaviour is solar-following — it charges from PV surplus when available and stays idle otherwise. No grid price arbitrage.

Purpose: Models what happens if HSEM only sets PV-charge recommendations without any grid-based scheduling.

Mathematical model:

  • Solar surplus slots (where estimated_net_consumption < 0) get batteries_charge_solar
  • All other charge/discharge/export recommendations cleared
  • Battery fills from PV, never from grid
  • Battery never discharges unless native inverter logic does so

MILP global optimisation

The MILP solver (planner/milp_optimizer.py) uses scipy's HiGHS to find the globally optimal charge/discharge schedule. This is the primary planner — the MILP solution is preferred over all heuristic candidates.

See MILP Optimization for the full LP formulation, variable layout, constraints, solver pipeline, and post-processing flow.

EV co-optimisation

When one or more active EVs are provided, the MILP co-optimises EV charging alongside the battery schedule. EV charging variables are added to the LP variable vector and the energy balance equation includes EV charger load.

Fallback

If scipy is unavailable or the solver fails (infeasible / numerical issue), the MILP candidate is silently dropped and the remaining candidates (no_action, passive) compete as normal.


Hysteresis

Plan-level hysteresis (issue #372)

Prevents the planner from switching strategies for tiny cost improvements. When active:

  1. The previously active plan is re-evaluated with current data
  2. If its score improvement over the best new candidate is below both thresholds, the previous plan is kept
Threshold Default Behaviour
Absolute 0.0 currency New plan must be cheaper by at least this amount
Percentage 5.0 % New plan must be cheaper by at least this % of previous score

Window-level hysteresis (issue #315)

Prevents rapid charge↔discharge toggles near schedule-window boundaries.

  • Charge-type: batteries_charge_grid, batteries_charge_solar, ev_smart_charging
  • Discharge-type: batteries_discharge_mode, force_batteries_discharge, force_export
  • Neutral: batteries_wait_mode, time_passed, missing_input_entities, None

Only cross-category transitions are held. The hold time is configured by planner_window_hysteresis_minutes (default: 0, disabled).

HSEM Documentation

Quick Reference

Architecture Decision Records

Clone this wiki locally