feat(optimization): deadlines for consumers and EV, graded grid export

Three related scheduling improvements, all opt-in and behaviour-preserving
when the new fields are not set.

Flexible consumers get absolute time bounds next to the recurring
time_windows: earliest_start_datetime and deadline_datetime, where the
deadline requires the complete run to have *finished* before that moment
("clean dishes by 03:00 tonight"). When no start can meet it,
deadline_policy decides between BEST_EFFORT (run as early as possible, so
the delay rather than the cost is minimized) and STRICT (keep the
deadline; a ONCE consumer then fails the optimization). The solution
reports appliance_deadline_missed per device.

The EV charging target can be given the same kind of deadline, as an
absolute min_soc_deadline_datetime and/or a relative min_soc_max_duration_h
("full in 6 hours"), the earlier of the two winning. The ev_soc_miss
penalty is then evaluated at that slot instead of at the end of the
horizon, and the seeding heuristic only proposes charge slots before it.

Battery-to-grid export under direct marketing is no longer all-or-nothing:
grid_export_rates configures the selectable export levels as a factor of
the rated discharge power (default [0.25, 0.5, 0.75, 1.0]). Each rate is
its own optimizer state, with the full-power state keeping its previous
index so existing seeds and heuristics are unaffected. The chosen level
per slot is reported in battery_grid_export_factor and as the
GRID_SUPPORT_EXPORT operation factor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Andreas
2026-09-03 17:53:33 +02:00
co-authored by Claude Opus 5
parent 8926cc7ae0
commit f24d9ea0eb
20 changed files with 1469 additions and 39 deletions
+14
View File
@@ -528,6 +528,20 @@ def test_direct_marketing_battery_grid_export_uses_separate_signal(config_eos):
assert simulation.battery.current_soc_percentage() == 50.0
def test_direct_marketing_grid_export_rate_limits_exported_energy(config_eos):
"""A partial export level exports that share of the rated discharge power."""
simulation = _direct_marketing_battery_export_simulation(config_eos)
assert simulation.bat_grid_export_hours is not None
# 500 W rated discharge power over a one hour slot -> 500 Wh at rate 1.0.
simulation.bat_grid_export_hours[0] = 0.5
result = simulation.simulate(start_hour=0)
assert result["Netzeinspeisung_Wh_pro_Stunde"][0] == pytest.approx(250.0)
assert simulation.battery is not None
assert simulation.battery.current_soc_percentage() == 75.0
def test_battery_lcos_is_charged_once_on_delivered_energy(config_eos):
simulation = _direct_marketing_battery_export_simulation(
config_eos,