Fix fixu gri charge
This commit is contained in:
@@ -163,12 +163,14 @@ def _select_charge_slots(
|
|||||||
|
|
||||||
Logika:
|
Logika:
|
||||||
1) Sloty s PV-surplus (pv_a + pv_b > load_baseline) jsou vždy zahrnuty –
|
1) Sloty s PV-surplus (pv_a + pv_b > load_baseline) jsou vždy zahrnuty –
|
||||||
jde o „zdarma“ nabíjení z FVE, nemá smysl ho zakazovat.
|
nabíjení z FVE je „zdarma“, solver ho musí mít povolené. Tyto sloty se
|
||||||
2) Zbývající energetický rozpočet (cíl = charge_buf × (soc_max − current_soc),
|
NEzapočítávají do grid rozpočtu (v dlouhém horizontu by přetekly target).
|
||||||
snížený o očekávaný přínos z PV-surplus slotů) se doplní nejlevnějšími sloty
|
2) Nezávisle na bodu 1 se vybere top-N **grid** slotů seřazených podle
|
||||||
podle buy_price (nákupní cena ze sítě).
|
`buy_price` ASC tak, aby pokryly `charge_buf × (soc_max − current_soc)`.
|
||||||
3) Per-slot kapacita přírůstku SoC = max_charge_power × η × 15 min (plný výkon,
|
Tím dostane solver k dispozici přístup k nejlevnějšímu nákupu ze sítě,
|
||||||
ne limitovaný aktuálním PV-surplus výkonem).
|
i když PV v daném slotu spotřebu nepokrývá.
|
||||||
|
3) Per-slot kapacita přírůstku SoC = max_charge_power × η × 15 min (plný
|
||||||
|
výkon, ne limitovaný aktuálním PV-surplus výkonem).
|
||||||
|
|
||||||
Vrací množinu indexů povolených pro `bc[t] > 0` v MILP. Prázdná množina = žádné
|
Vrací množinu indexů povolených pro `bc[t] > 0` v MILP. Prázdná množina = žádné
|
||||||
restrikce. `charge_slot_buffer <= 0` v DB ⇒ všechny sloty povoleny.
|
restrikce. `charge_slot_buffer <= 0` v DB ⇒ všechny sloty povoleny.
|
||||||
@@ -186,17 +188,13 @@ def _select_charge_slots(
|
|||||||
per_slot_full_wh = max_p_w * eta * INTERVAL_H
|
per_slot_full_wh = max_p_w * eta * INTERVAL_H
|
||||||
|
|
||||||
selected: set[int] = set()
|
selected: set[int] = set()
|
||||||
pv_budget_wh = 0.0
|
|
||||||
for t, s in enumerate(slots):
|
for t, s in enumerate(slots):
|
||||||
pv_surplus_w = max(0, s.pv_a_forecast_w + s.pv_b_forecast_w - s.load_baseline_w)
|
pv_surplus_w = max(0, s.pv_a_forecast_w + s.pv_b_forecast_w - s.load_baseline_w)
|
||||||
if pv_surplus_w <= 0:
|
if pv_surplus_w > 0:
|
||||||
continue
|
selected.add(t)
|
||||||
selected.add(t)
|
|
||||||
pv_budget_wh += min(float(pv_surplus_w), max_p_w) * eta * INTERVAL_H
|
|
||||||
|
|
||||||
target_wh = energy_to_fill * charge_buf
|
grid_target_wh = energy_to_fill * charge_buf
|
||||||
remaining_wh = max(0.0, target_wh - pv_budget_wh)
|
if grid_target_wh <= 0 or per_slot_full_wh <= 0:
|
||||||
if remaining_wh <= 0 or per_slot_full_wh <= 0:
|
|
||||||
return selected
|
return selected
|
||||||
|
|
||||||
grid_candidates = [
|
grid_candidates = [
|
||||||
@@ -206,7 +204,7 @@ def _select_charge_slots(
|
|||||||
|
|
||||||
cumulative = 0.0
|
cumulative = 0.0
|
||||||
for t, _price in grid_candidates:
|
for t, _price in grid_candidates:
|
||||||
if cumulative >= remaining_wh:
|
if cumulative >= grid_target_wh:
|
||||||
break
|
break
|
||||||
selected.add(t)
|
selected.add(t)
|
||||||
cumulative += per_slot_full_wh
|
cumulative += per_slot_full_wh
|
||||||
|
|||||||
@@ -99,6 +99,32 @@ class SelectChargeSlotsTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_long_horizon_pv_surplus_does_not_exhaust_grid_budget(self) -> None:
|
||||||
|
"""Regrese: v 96h horizontu nesmí PV-surplus sloty „vyžrat“ grid rozpočet.
|
||||||
|
|
||||||
|
V dřívější verzi se kumulativní PV budget odečítal od `charge_buf × headroom`,
|
||||||
|
takže v dlouhém horizontu s mnoha PV sloty zbylo 0 na grid filler a levné
|
||||||
|
grid sloty se nepovolily. Tento test simuluje realistický 96h profil.
|
||||||
|
"""
|
||||||
|
# 40 levných grid-only slotů (simulace noční / „inter-peak“ hodiny).
|
||||||
|
cheap_grid = [_slot(buy=0.4 + 0.01 * i, pv=0, load=2_000) for i in range(40)]
|
||||||
|
# 100 PV-surplus slotů s velkou výrobou (přes den, přes víc dní).
|
||||||
|
pv_days = [_slot(buy=1.5, pv=10_000, load=2_000) for _ in range(100)]
|
||||||
|
slots = cheap_grid + pv_days
|
||||||
|
battery = _battery(
|
||||||
|
charge_buf=1.3, uc_wh=64_000.0, soc_max_pct=95.0, max_charge_w=18_000.0
|
||||||
|
)
|
||||||
|
out = _select_charge_slots(slots, battery, current_soc_wh=0.2 * battery.usable_capacity_wh)
|
||||||
|
grid_selected = sum(1 for i in range(len(cheap_grid)) if i in out)
|
||||||
|
self.assertGreaterEqual(
|
||||||
|
grid_selected,
|
||||||
|
5,
|
||||||
|
msg=(
|
||||||
|
"V dlouhém horizontu s mnoha PV-surplus sloty musí zůstat dostatek "
|
||||||
|
"grid slotů povolených pro nabíjení z levného importu."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_energy_budget_is_charge_buf_times_headroom(self) -> None:
|
def test_energy_budget_is_charge_buf_times_headroom(self) -> None:
|
||||||
"""Součet uvolněné energie se pohybuje okolo charge_buf × (soc_max − current_soc)."""
|
"""Součet uvolněné energie se pohybuje okolo charge_buf × (soc_max − current_soc)."""
|
||||||
slots = [_slot(buy=float(i + 1), pv=0, load=2_000) for i in range(40)]
|
slots = [_slot(buy=float(i + 1), pv=0, load=2_000) for i in range(40)]
|
||||||
|
|||||||
Reference in New Issue
Block a user