mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-16 02:51:56 +00:00
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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user