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.
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.