zruseni fixnich konstant
Some checks failed
CI and deploy / migration-check (push) Failing after 20s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-25 09:41:06 +02:00
parent 37a525cb4f
commit f1a4dbd7e7
4 changed files with 153 additions and 26 deletions

View File

@@ -11,6 +11,8 @@ from services.planning_engine import (
PlanningSlot,
_dynamic_arb_floor_wh_series,
_dispatch_result_comparison,
_evening_battery_export_push_indices,
_evening_push_discharge_budget_wh,
_pre_neg_peak_sell_idx,
_prague_hour,
_prewindow_deferral_slots,
@@ -50,6 +52,7 @@ def _battery(
arb_pct: float = 20.0,
max_pct: float = 95.0,
terminal_soc_value_factor: float = 0.9,
discharge_slot_buffer: float = 1.5,
) -> SimpleNamespace:
uc = uc_wh
min_wh = min_pct / 100.0 * uc
@@ -65,10 +68,79 @@ def _battery(
degradation_cost_czk_kwh=0.15,
max_charge_power_w=10_000,
max_discharge_power_w=10_000,
discharge_slot_buffer=discharge_slot_buffer,
planner_terminal_soc_value_factor=terminal_soc_value_factor,
)
class EveningPushBudgetTests(unittest.TestCase):
"""Večerní tvrdý push: počet slotů z rozpočtu Wh (ne pevné top-3)."""
@staticmethod
def _evening_slots(n: int = 8) -> list[PlanningSlot]:
base = datetime(2026, 5, 25, 15, 0, tzinfo=timezone.utc)
slots: list[PlanningSlot] = []
for i in range(n):
slots.append(
PlanningSlot(
interval_start=base + timedelta(minutes=15 * i),
buy_price=2.0,
sell_price=4.0 + 0.01 * i,
pv_a_forecast_w=0,
pv_b_forecast_w=0,
load_baseline_w=1000,
ev1_connected=False,
ev2_connected=False,
allow_discharge_export=True,
charge_acquisition_buy_czk_kwh=0.5,
)
)
return slots
def test_budget_scales_with_soc_not_fixed_three(self) -> None:
slots = self._evening_slots(8)
per_slot = 17_000 * 0.95 * 0.25
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
soc_high = 0.92 * bat.soc_max_wh
profitable = set(range(len(slots)))
push_hi = _evening_battery_export_push_indices(
slots,
profitable_export_ts=profitable,
degrad_czk_kwh=0.15,
current_soc_wh=soc_high,
min_soc_wh=bat.min_soc_wh,
soc_max_wh=bat.soc_max_wh,
per_slot_discharge_wh=per_slot,
discharge_slot_buffer=1.5,
)
self.assertGreater(len(push_hi), 3)
soc_low = bat.min_soc_wh + 100.0
push_lo = _evening_battery_export_push_indices(
slots,
profitable_export_ts=profitable,
degrad_czk_kwh=0.15,
current_soc_wh=soc_low,
min_soc_wh=bat.min_soc_wh,
soc_max_wh=bat.soc_max_wh,
per_slot_discharge_wh=per_slot,
discharge_slot_buffer=1.5,
)
self.assertEqual(len(push_lo), 0)
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
budget = _evening_push_discharge_budget_wh(
current_soc_wh=soc,
min_soc_wh=bat.min_soc_wh,
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
self.assertAlmostEqual(budget, min(available, exportable_full * 1.5))
class SlotsUntilSellNegativeTests(unittest.TestCase):
def test_slots_until_first_negative_sell(self) -> None:
base = datetime(2026, 4, 3, 0, 0, tzinfo=timezone.utc)
@@ -1230,7 +1302,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
50.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-pre-neg-batt-discharge-v23")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-evening-push-dynamic-budget-v24")
self.assertGreater(
results[0].battery_setpoint_w,
5_500,
@@ -1380,7 +1452,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
50.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-pre-neg-batt-discharge-v23")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-evening-push-dynamic-budget-v24")
self.assertEqual(len(results), len(slots))
def test_gen_cutoff_full_soc_neg_sell_with_pv_b_feasible(self) -> None:
@@ -1444,7 +1516,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
55.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-pre-neg-batt-discharge-v23")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-evening-push-dynamic-budget-v24")
self.assertEqual(len(results), len(slots))
def test_fixed_tariff_neg_sell_no_grid_export(self) -> None: