Files
EOS/docs/akkudoktoreos/optimization_horizons.md
T
Andreas a2f4ef6f54 feat(optimization): split the control horizon from the forecast tail
The optimizer treated the end of `optimization.horizon_hours` as the end of the
world: energy left in the battery there was worth a single configured price per
kWh, so it either dumped the battery into the last hours or hoarded it,
depending on that one number.

The horizon is now two spans. `horizon_hours` still receives every control
command. The new `optimization.tail_horizon_hours` (default 48 h) is a pure
lookahead that never produces a command. In AUTO terminal-value mode a
deterministic dynamic program solves that tail backwards on a 101-point SoC
grid using the production battery and inverter models - SoC bounds, power caps,
conversion losses, configured charge and export rates, direct-marketing
permission and LCOS on delivered DC energy - and the existing AUTO proxy
supplies the continuation value at the tail end. Genetic fitness reads the
resulting curve. `tail_horizon_hours: 0` restores the plain proxy at the control
end, FIXED is unchanged.

The forecast budget is reported, never enforced by refusal: a tail that does not
fit is shortened to what the forecast covers and reported as
`effective_tail_hours`, and a control horizon that does not fit is warned about
at configuration time and rejected by the optimizer at run time, which knows
which series ran out. `prediction.hours` defaults to 72 so the new defaults fit
out of the box; existing shorter configurations keep starting.

Control arrays and warm-start genomes now begin at the run timestamp rather than
midnight, flagged by `controls_start_at_now` so the adapters still read older
solutions. `forecast_interval_seconds` declares the resolution of shortened
native quarter-hour inputs.

Required forecasts are no longer silently replaced by demo providers. A missing
PV, price, load, feed-in or weather forecast used to rewrite the configured
provider and retry, so a run could quietly optimize against invented data.
Missing values now stay missing, and provider values are held only within their
own source interval instead of being extended indefinitely.

Also fixes a config update that could leave EOS half-updated: the merged
candidate is validated before the singleton is reinitialized.

Four provider tests that hard-coded the old 48 h prediction default are rewritten
to derive their expectations from the configured horizon.
2026-09-09 07:56:38 +02:00

5.8 KiB

Control horizon and battery lookahead

The genetic optimizer issues controls only for optimization.horizon_hours, measured from the run start. The default is 24 hours. Battery and EV genomes, appliance schedules, simulation costs and returned control arrays all stop at that boundary. A larger prediction horizon does not add control genes.

{
  "optimization": {
    "horizon_hours": 24,
    "tail_horizon_hours": 48,
    "terminal_value_mode": "AUTO"
  },
  "prediction": {"hours": 72}
}

The forecast budget is never enforced by rejecting the configuration. prediction.hours also serves callers that do not optimize at all, so a budget that cannot serve the optimization horizons is reported rather than refused.

A prediction.hours below horizon_hours + tail_horizon_hours shortens the tail to prediction.hours - horizon_hours, logged once at INFO and reported per run as effective_tail_hours. A prediction.hours below horizon_hours is logged as a warning at configuration time and rejected by the optimizer itself when a run starts, naming the forecast series that ends too early. EOS never rewrites the settings; raise prediction.hours to use the requested horizons in full.

Tail value and continuation

In AUTO mode, a deterministic dynamic program evaluates the battery state at the end of the control horizon against the following forecast intervals. It uses a 101-point SOC grid and interpolates between states. Each transition uses the production battery and inverter models, including SOC bounds, power caps, conversion losses, configured charging/export rates, PV, load, direct-marketing permission and LCOS on delivered DC energy.

The value curve is computed once per run. Genetic fitness subtracts its interpolated value from the simulated control cost. The older AC break-even penalty is disabled in TAIL mode because it does not account for future tail opportunities. Other feasibility penalties, including EV targets, still apply.

The curve may decrease with SOC: free capacity can earn money at negative prices. Even an empty battery can have nonzero value. Values include net tail cash flows and the continuation credit; this constant baseline does not affect which control plan wins within a run. Tail actions are never returned.

The existing AUTO proxy supplies the continuation value at the effective tail end. Its trailing window is controlled by terminal_value_window_hours. A window with no valuable load or export opportunities conservatively has zero continuation credit. With tail_horizon_hours: 0, AUTO uses the existing proxy directly at the control end. FIXED preserves the scalar terminal credit directly at the control end and does not solve a tail.

This is a deterministic approximation on a discretized SOC grid, using the configured action levels. It does not model forecast uncertainty or schedule additional EV/appliance activity beyond the control horizon.

Forecast availability and API indexing

The four required series are load, PV, import price and feed-in tariff. Every control interval must contain a finite value. Missing control data rejects the run. The tail stops at the first missing value in any required series, logs a warning and moves continuation to that point. No missing price becomes zero. Provider interval values are held within their source interval, never extended indefinitely beyond the last observed forecast timestamp.

Legacy API forecast arrays still begin at midnight of the run's start day. They must therefore include the elapsed prefix plus the desired forecast coverage from now. The prefix is removed before optimization. Declare forecast_interval_seconds: 900 for shortened native quarter-hour inputs; hourly inputs use 3600. Fully sized native arrays remain auto-detected for compatibility. Scalar feed-in tariffs represent an explicitly constant tariff.

New solutions set controls_start_at_now: true: index zero of every returned control array and warm-start genome corresponds to the run timestamp. The generic solution and plan adapters still understand older, midnight-indexed solutions where this flag is absent. Incompatible old genome lengths are discarded by warm-start validation.

terminal_value reports the mode (TAIL, AUTO, or FIXED), usable remaining AC energy, control hours, requested/effective tail hours, continuation mode and any degradation reason. tail_end_hour is elapsed hours from the run start. FIXED mode reports zero effective tail hours.

The credited amount is explicitly decomposed:

{
  "mode": "TAIL",
  "battery_energy_wh": 5230,
  "credited_euro": 1.84,
  "tail_operating_euro": 1.21,
  "continuation_value_euro": 0.63,
  "control_horizon_hours": 24,
  "requested_tail_hours": 48,
  "effective_tail_hours": 48,
  "tail_end_hour": 72,
  "continuation_mode": "AUTO",
  "curve": {
    "energy_wh": [],
    "value_euro": [],
    "operating_value_euro": [],
    "continuation_value_euro": [],
    "marginal_euro_per_kwh": []
  },
  "continuation_curve": {
    "energy_wh": [],
    "value_euro": [],
    "marginal_euro_per_kwh": []
  },
  "tail_diagnostics": {
    "slots": 192,
    "slot_hours": 0.25,
    "soc_grid_points": 101,
    "min_import_price_euro_per_kwh": -0.08,
    "max_import_price_euro_per_kwh": 0.34,
    "min_feed_in_tariff_euro_per_kwh": 0.0,
    "max_feed_in_tariff_euro_per_kwh": 0.29,
    "negative_import_price_slots": 4,
    "positive_battery_export_slots": 160
  }
}

credited_euro always equals tail_operating_euro + continuation_value_euro. The combined curve is the function read by genetic fitness. It contains the same decomposition at every SOC breakpoint. continuation_curve is the proxy at the effective tail end before the dynamic program folds the tail backwards onto it. Tail diagnostics summarize the actual forecast segment; they do not contain executable actions.