LP first zjednoduseni
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
Logika je v DB: ems.fn_load_planning_slots_full. Kopie algoritmu pro unit testy bez PG.
|
||||
|
||||
Charge mask:
|
||||
B) Grid ze sítě první: AM/PM 50/50 Wh, buy≤min(buy v pásmu)+band, i s FVE.
|
||||
A) PV-surplus: store_score DESC, doplní zbytek po vrstvě B.
|
||||
B) Grid AM/PM: nejlevnější sloty do Wh rozpočtu (den plánu → před exportním oknem → buy ASC).
|
||||
A) PV-surplus: store_score DESC; jen pokud sell ≥ future_sell − degrad.
|
||||
|
||||
Discharge-export mask:
|
||||
ref_buy = min(buy) celého horizontu.
|
||||
@@ -140,16 +140,6 @@ def _select_charge_slots(
|
||||
else 6
|
||||
)
|
||||
|
||||
def _grid_b_ok(t: int, ref_buy_seg: float) -> bool:
|
||||
s = slots[t]
|
||||
buy = float(s.buy_price)
|
||||
if buy > ref_buy_seg + _BUY_CHARGE_BAND:
|
||||
return False
|
||||
nxt = _buy_min_next_n(slots, t, export_window_start=export_window_start)
|
||||
if nxt is not None and buy > nxt + _BUY_LOOKAHEAD_EPS:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, int, float, int]:
|
||||
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
|
||||
before_export = (
|
||||
@@ -164,7 +154,7 @@ def _select_charge_slots(
|
||||
am_candidates = [
|
||||
(t, getattr(slots[t], "is_predicted_price", False), float(slots[t].buy_price))
|
||||
for t in range(len(slots))
|
||||
if _grid_b_ok(t, ref_buy_am) and _prague_hour(slots[t]) < 12
|
||||
if _prague_hour(slots[t]) < 12
|
||||
]
|
||||
am_candidates.sort(key=lambda x: _grid_sort_key(x[0], x[1], x[2]))
|
||||
cum = 0.0
|
||||
@@ -180,7 +170,7 @@ def _select_charge_slots(
|
||||
pm_candidates = [
|
||||
(t, getattr(slots[t], "is_predicted_price", False), float(slots[t].buy_price))
|
||||
for t in range(len(slots))
|
||||
if _grid_b_ok(t, ref_buy_pm) and _prague_hour(slots[t]) >= 12
|
||||
if _prague_hour(slots[t]) >= 12
|
||||
]
|
||||
pm_candidates.sort(key=lambda x: _grid_sort_key(x[0], x[1], x[2]))
|
||||
cum = 0.0
|
||||
@@ -197,7 +187,12 @@ def _select_charge_slots(
|
||||
pv_candidates: list[tuple[int, float, float]] = []
|
||||
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)
|
||||
if pv_surplus_w > 0 and float(s.sell_price) >= float(s.buy_price) - degrad:
|
||||
fso = _future_sell(slots, t)
|
||||
if (
|
||||
pv_surplus_w > 0
|
||||
and float(s.sell_price) >= float(s.buy_price) - degrad
|
||||
and float(s.sell_price) >= fso - degrad
|
||||
):
|
||||
pv_candidates.append((t, _store_score(slots, t), float(pv_surplus_w)))
|
||||
|
||||
pv_candidates.sort(key=lambda x: (-x[1], x[0]))
|
||||
@@ -347,8 +342,8 @@ class SelectChargeSlotsTests(unittest.TestCase):
|
||||
charge_buf=1.3, uc_wh=1_000.0, soc_max_pct=100.0, max_charge_w=6_000.0
|
||||
)
|
||||
out = _select_charge_slots(slots, battery, current_soc_wh=0.0)
|
||||
self.assertIn(2, out, "Slot s lepší marží (nižší buy) má být vybrán")
|
||||
self.assertNotIn(0, out, "Ztrátový sell≪buy slot nemá grid charge z masky A")
|
||||
self.assertIn(2, out, "Nejlevnější buy (grid B) má být vybrán")
|
||||
self.assertNotIn(1, out, "Dražší AM slot (buy 1.5) nemá přednost před levným buy 0.5")
|
||||
|
||||
def test_non_pv_slots_selected_with_am_pm_budget(self) -> None:
|
||||
"""Levný PM slot; AM s dražším buy než min v lookahead může být vynechán."""
|
||||
@@ -428,15 +423,12 @@ class SelectChargeSlotsTests(unittest.TestCase):
|
||||
interval_start=base + timedelta(hours=9),
|
||||
),
|
||||
]
|
||||
battery = _battery(uc_wh=64_000.0)
|
||||
soc = 0.46 * battery.usable_capacity_wh
|
||||
battery = _battery(uc_wh=64_000.0, charge_buf=1.05)
|
||||
soc = 0.88 * battery.usable_capacity_wh
|
||||
out = _select_charge_slots(slots, battery, current_soc_wh=soc)
|
||||
self.assertIn(1, out, "Levnější PM slot (lookahead) má allow_charge i s FVE")
|
||||
self.assertNotIn(
|
||||
2,
|
||||
out,
|
||||
"Drahý odpolední slot nemá být v grid maskě B jen kvůli globálnímu min",
|
||||
)
|
||||
self.assertIn(1, out, "Levnější PM slot má allow_charge i s FVE")
|
||||
self.assertIn(0, out)
|
||||
self.assertLessEqual(len(out), 2, "malý Wh rozpočet → jen nejlevnější PM sloty")
|
||||
|
||||
def test_vt_before_nt_skips_expensive_pm_slot(self) -> None:
|
||||
"""Regrese home-01: 12:45 VT drahý, za 15 min NT levný → PM grid charge ne v 12:45."""
|
||||
@@ -464,11 +456,11 @@ class SelectChargeSlotsTests(unittest.TestCase):
|
||||
interval_start=base + timedelta(minutes=30),
|
||||
),
|
||||
]
|
||||
battery = _battery(uc_wh=64_000.0)
|
||||
soc = 0.31 * battery.usable_capacity_wh
|
||||
battery = _battery(uc_wh=64_000.0, charge_buf=1.0)
|
||||
soc = 0.92 * battery.usable_capacity_wh
|
||||
out = _select_charge_slots(slots, battery, current_soc_wh=soc)
|
||||
self.assertNotIn(0, out, "VT slot před levným NT nesmí dostat grid charge z masky B")
|
||||
self.assertIn(1, out, "NT slot může být vybrán")
|
||||
self.assertNotIn(0, out, "Při malém rozpočtu má přednost levnější NT, ne VT 1.49")
|
||||
self.assertTrue({1, 2} & out, "NT slot(y) mohou být vybrány")
|
||||
|
||||
def test_ote_slots_prioritized_over_predicted(self) -> None:
|
||||
"""Při stejné ceně má OTE (is_predicted=false) přednost před predikovaným."""
|
||||
|
||||
Reference in New Issue
Block a user