oprava nevyberu maximalnich sell slotu (sahal i na zitejsi vecer)
This commit is contained in:
@@ -17,7 +17,9 @@ from services.planning_engine import (
|
||||
_evening_peak_export_indices,
|
||||
_slot_evening_push_profitable,
|
||||
_evening_push_calendar_segments,
|
||||
_evening_push_soc_budget_calendar_segments,
|
||||
_evening_push_discharge_budget_wh,
|
||||
_primary_night_export_segment_indices,
|
||||
_in_evening_push_hour_window,
|
||||
_in_night_battery_export_window,
|
||||
_neg_sell_day_phases,
|
||||
@@ -285,28 +287,64 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(push, [])
|
||||
|
||||
def test_per_calendar_evening_push_budget_split(self) -> None:
|
||||
"""Dva večery v horizontu → každý dostane část Wh rozpočtu (druhý den ne prázdný)."""
|
||||
def test_evening_push_budget_only_primary_night_episode(self) -> None:
|
||||
"""v49: Wh z current_soc jen pro první noční epizodu — ne zítřejší večer po dni FVE."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
slots: list[PlanningSlot] = []
|
||||
for day in (25, 26):
|
||||
for h, m in ((18, 0), (18, 15), (18, 30)):
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, day, h, m, tzinfo=prague),
|
||||
buy_price=5.0,
|
||||
sell_price=4.0 + 0.1 * (h - 18),
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=800,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
for h, m in ((18, 0), (18, 15), (18, 30)):
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, 25, h, m, tzinfo=prague),
|
||||
buy_price=5.0,
|
||||
sell_price=4.0 + 0.1 * (h - 18),
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=800,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
segs = _evening_push_calendar_segments(slots, discharge_export_ok=set(range(len(slots))))
|
||||
self.assertEqual(len(segs), 2)
|
||||
)
|
||||
# Denní FVE mezi večery → druhá noční epizoda (zítřejší večer nesmí brát SoC rozpočet).
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, 26, 11, 0, tzinfo=prague),
|
||||
buy_price=3.0,
|
||||
sell_price=2.0,
|
||||
pv_a_forecast_w=3000,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=800,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
)
|
||||
for h, m in ((18, 0), (18, 15), (18, 30)):
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, 26, h, m, tzinfo=prague),
|
||||
buy_price=5.0,
|
||||
sell_price=4.0 + 0.1 * (h - 18),
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=800,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(_evening_push_calendar_segments(slots, set(range(len(slots))))), 2)
|
||||
n = len(slots)
|
||||
budget_segs = _evening_push_soc_budget_calendar_segments(
|
||||
slots, discharge_export_ok=set(range(n))
|
||||
)
|
||||
self.assertEqual(len(budget_segs), 1)
|
||||
self.assertTrue(all(slots[t].interval_start.day == 25 for seg in budget_segs for t in seg))
|
||||
primary = _primary_night_export_segment_indices(slots)
|
||||
self.assertEqual(primary, {0, 1, 2})
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
per_slot = 13_500 * 0.95 * 0.25
|
||||
push = _evening_battery_export_push_indices(
|
||||
@@ -318,24 +356,25 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
discharge_export_ok=set(range(len(slots))),
|
||||
discharge_export_ok=set(range(n)),
|
||||
)
|
||||
day25 = {t for t in push if slots[t].interval_start.day == 25}
|
||||
day26 = {t for t in push if slots[t].interval_start.day == 26}
|
||||
self.assertGreaterEqual(len(day25), 1)
|
||||
self.assertGreaterEqual(len(day26), 1)
|
||||
self.assertEqual(day26, set(), "zítřejší večer nesmí krást dnešní Wh rozpočet")
|
||||
|
||||
def test_evening_push_budget_matches_r063_formula(self) -> None:
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
soc = 0.85 * bat.soc_max_wh
|
||||
floor = max(bat.min_soc_wh, bat.reserve_soc_wh)
|
||||
budget = _evening_push_discharge_budget_wh(
|
||||
current_soc_wh=soc,
|
||||
min_soc_wh=bat.min_soc_wh,
|
||||
discharge_floor_wh=floor,
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
discharge_slot_buffer=1.5,
|
||||
)
|
||||
exportable_full = bat.soc_max_wh - bat.min_soc_wh
|
||||
available = soc - bat.min_soc_wh
|
||||
exportable_full = bat.soc_max_wh - floor
|
||||
available = soc - floor
|
||||
self.assertAlmostEqual(budget, min(available, exportable_full * 1.5))
|
||||
|
||||
def test_push_slot_count_follows_wh_budget_not_fixed_top_n(self) -> None:
|
||||
@@ -360,26 +399,27 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
]
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
per_slot = 17_000 * 0.95 * 0.25
|
||||
soc_three_slots = bat.min_soc_wh + 3.2 * per_slot
|
||||
floor = bat.min_soc_wh
|
||||
soc_for_budget = floor + 3.2 * per_slot
|
||||
budget = _evening_push_discharge_budget_wh(
|
||||
current_soc_wh=soc_three_slots,
|
||||
min_soc_wh=bat.min_soc_wh,
|
||||
current_soc_wh=soc_for_budget,
|
||||
discharge_floor_wh=floor,
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
discharge_slot_buffer=1.5,
|
||||
)
|
||||
expected_n = min(3, max(0, int(budget // per_slot)))
|
||||
expected_n = max(0, int(budget // per_slot))
|
||||
push = _evening_battery_export_push_indices(
|
||||
slots,
|
||||
charge_acquisition_czk_kwh=0.5,
|
||||
degrad_czk_kwh=0.15,
|
||||
current_soc_wh=soc_three_slots,
|
||||
min_soc_wh=bat.min_soc_wh,
|
||||
current_soc_wh=soc_for_budget,
|
||||
min_soc_wh=floor,
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
)
|
||||
self.assertEqual(len(push), expected_n)
|
||||
self.assertEqual(push, [0, 1, 2], "nejdražší sloty první, ne jeden slot")
|
||||
self.assertEqual(push[:3], [0, 1, 2], "nejdražší sloty první")
|
||||
self.assertNotIn(3, push)
|
||||
# Více SoC → více push slotů (dynamicky, ne strop 3).
|
||||
push_hi = _evening_battery_export_push_indices(
|
||||
|
||||
Reference in New Issue
Block a user