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

134 lines
5.8 KiB
Markdown

# 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.
```json
{
"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:
```json
{
"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.