v2: noční SoC polštář — placená rezerva na neočekávaný noční nákup
Postřeh uživatele: v1 držel přes noc rezervu nad min_soc (chyba predikce noční spotřeby = neplánovaný drahý nákup); v2 slot fieldy night_baseload_* ignoroval a směl plánovat vybití až na min_soc. Mechanismus ve filozofii v2 (riziko jako cena, ne okno/penalta): soft floor soc[t] >= min_soc + night_baseload_buffer_wh[t] (z DB planner_night_baseload_buffer_percent, počítá R__063, klesá k 0 do rána); porušení placené buy cenou slotu → extrémní sell špička smí polštář racionálně prodat, běžná noc ne (buy > sell). Eval na fixtures: v2 stále lepší na všech (+221.9 Kč vs v1; −10 Kč proti stavu bez polštáře = cena robustnosti). BONUS: těsnější LP zrychlil extrémní fixtures z 10 s timeoutu na 0.3–2.6 s. +3 testy (drží/spike prodá/feasible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,45 @@ class OperatingModeTests(unittest.TestCase):
|
||||
self.assertLessEqual(r.grid_setpoint_w, 1000, "import ≤ baseline load")
|
||||
|
||||
|
||||
class NightReserveTests(unittest.TestCase):
|
||||
def test_night_discharge_respects_buffer(self) -> None:
|
||||
# noc: vysoký sell, žádné PV; buffer 2000 Wh nad min → plán nesmí
|
||||
# kalkulovat s vybitím pod min+buffer (sell < buy ⇒ slack se nevyplatí)
|
||||
bat = _battery()
|
||||
slots = []
|
||||
for i in range(16):
|
||||
s = _slot(_BASE, i, buy=6.0, sell=4.5, load=800)
|
||||
s.night_baseload_buffer_wh = 2000.0
|
||||
slots.append(s)
|
||||
results, _, _ = _solve(slots, battery=bat, soc0=0.6 * bat.usable_capacity_wh)
|
||||
floor_pct = (bat.min_soc_wh + 2000.0) / bat.usable_capacity_wh * 100.0
|
||||
for r in results:
|
||||
self.assertGreaterEqual(r.battery_soc_target, floor_pct - 0.6)
|
||||
|
||||
def test_extreme_sell_spike_may_sell_reserve(self) -> None:
|
||||
# sell výrazně nad buy → racionální polštář prodat (placený slack)
|
||||
bat = _battery()
|
||||
slots = []
|
||||
for i in range(16):
|
||||
s = _slot(_BASE, i, buy=2.0, sell=12.0, load=300)
|
||||
s.night_baseload_buffer_wh = 2000.0
|
||||
slots.append(s)
|
||||
results, _, _ = _solve(slots, battery=bat, soc0=0.6 * bat.usable_capacity_wh)
|
||||
min_soc_pct = min(r.battery_soc_target for r in results)
|
||||
floor_pct = (bat.min_soc_wh + 2000.0) / bat.usable_capacity_wh * 100.0
|
||||
self.assertLess(min_soc_pct, floor_pct - 1.0, "spike má polštář vyprodat")
|
||||
|
||||
def test_start_below_buffer_is_feasible(self) -> None:
|
||||
bat = _battery()
|
||||
slots = []
|
||||
for i in range(8):
|
||||
s = _slot(_BASE, i, buy=6.0, sell=1.0, load=1500)
|
||||
s.night_baseload_buffer_wh = 3000.0
|
||||
slots.append(s)
|
||||
results, _, _ = _solve(slots, battery=bat, soc0=bat.min_soc_wh + 500.0)
|
||||
self.assertEqual(len(results), 8)
|
||||
|
||||
|
||||
class EvDeadlineTests(unittest.TestCase):
|
||||
def test_ev_energy_delivered_before_deadline(self) -> None:
|
||||
slots = [_slot(_BASE, i, buy=2.0 if i < 8 else 6.0, sell=1.0, ev1=True) for i in range(16)]
|
||||
|
||||
Reference in New Issue
Block a user