prepsani s opusem dle planu
This commit is contained in:
@@ -233,6 +233,10 @@ class PlanningDispatchMilpTests(unittest.TestCase):
|
||||
effective_buy_price=1.0,
|
||||
effective_sell_price=1.0,
|
||||
is_predicted_price=False,
|
||||
cashflow_czk=1.0,
|
||||
battery_arbitrage_czk=0.0,
|
||||
penalty_czk=0.0,
|
||||
green_bonus_czk=0.0,
|
||||
)
|
||||
]
|
||||
peer = [
|
||||
@@ -256,6 +260,10 @@ class PlanningDispatchMilpTests(unittest.TestCase):
|
||||
effective_buy_price=1.0,
|
||||
effective_sell_price=1.0,
|
||||
is_predicted_price=False,
|
||||
cashflow_czk=2.0,
|
||||
battery_arbitrage_czk=0.0,
|
||||
penalty_czk=0.0,
|
||||
green_bonus_czk=0.0,
|
||||
)
|
||||
]
|
||||
cmp = _dispatch_result_comparison(active, 10, "v1", peer, 12, "v2")
|
||||
@@ -1222,7 +1230,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-26-neg-sell-bat-dump-extreme-buy-v11")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
|
||||
self.assertGreater(
|
||||
results[0].battery_setpoint_w,
|
||||
5_500,
|
||||
@@ -1372,7 +1380,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-26-neg-sell-bat-dump-extreme-buy-v11")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
|
||||
self.assertEqual(len(results), len(slots))
|
||||
|
||||
def test_gen_cutoff_full_soc_neg_sell_with_pv_b_feasible(self) -> None:
|
||||
@@ -1436,7 +1444,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
55.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-26-neg-sell-bat-dump-extreme-buy-v11")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
|
||||
self.assertEqual(len(results), len(slots))
|
||||
|
||||
def test_fixed_tariff_neg_sell_no_grid_export(self) -> None:
|
||||
@@ -2583,6 +2591,116 @@ class Home01RegressionTests(unittest.TestCase):
|
||||
self.assertGreaterEqual(results[i].grid_setpoint_w, -50)
|
||||
self.assertNotEqual(results[i].export_mode, "PV_SURPLUS")
|
||||
|
||||
@staticmethod
|
||||
def _home01_run16522_slots() -> list[PlanningSlot]:
|
||||
from test_planning_charge_slot_selection import (
|
||||
_battery as mask_battery,
|
||||
_select_charge_slots,
|
||||
_select_discharge_export_slots,
|
||||
)
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 24, 0, 0, tzinfo=prague)
|
||||
hour_specs: list[tuple[int, int, dict]] = [
|
||||
(0, 5, {"buy": 4.7, "sell": 2.9}),
|
||||
(5, 7, {"buy": 5.0, "sell": 3.0, "pv_b": 400}),
|
||||
(7, 11, {"buy": 4.5, "sell": 2.8, "pv_a": 3000, "pv_b": 2000}),
|
||||
(11, 14, {"buy": 0.5, "sell": -0.4, "pv_a": 6000, "pv_b": 5000}),
|
||||
(14, 17, {"buy": 1.0, "sell": -0.3, "pv_a": 5000, "pv_b": 4000}),
|
||||
(17, 19, {"buy": 4.5, "sell": 3.0}),
|
||||
(19, 22, {"buy": 6.5, "sell": 4.0}),
|
||||
(22, 24, {"buy": 4.8, "sell": 3.0}),
|
||||
]
|
||||
slots: list[PlanningSlot] = []
|
||||
for h0, h1, kw in hour_specs:
|
||||
for h in range(h0, h1):
|
||||
for minute in (0, 15, 30, 45):
|
||||
t = base.replace(hour=h, minute=minute).astimezone(timezone.utc)
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=t,
|
||||
buy_price=float(kw["buy"]),
|
||||
sell_price=float(kw["sell"]),
|
||||
pv_a_forecast_w=int(kw.get("pv_a", 0)),
|
||||
pv_b_forecast_w=int(kw.get("pv_b", 0)),
|
||||
load_baseline_w=500,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
is_predicted_price=False,
|
||||
)
|
||||
)
|
||||
mb = mask_battery(charge_buf=1.3, uc_wh=64_000.0, soc_max_pct=95.0)
|
||||
soc0 = 30_000.0
|
||||
charge = _select_charge_slots(slots, mb, soc0)
|
||||
discharge = _select_discharge_export_slots(slots, mb, soc0, charge)
|
||||
acq = (
|
||||
sum(float(slots[t].buy_price) for t in charge) / len(charge)
|
||||
if charge
|
||||
else min(float(s.buy_price) for s in slots)
|
||||
)
|
||||
cutoff = min(
|
||||
(slots[t].interval_start for t in discharge),
|
||||
default=slots[-1].interval_start,
|
||||
)
|
||||
for t, s in enumerate(slots):
|
||||
s.allow_charge = t in charge or float(s.buy_price) < 0
|
||||
s.allow_discharge_export = t in discharge
|
||||
s.charge_acquisition_buy_czk_kwh = acq
|
||||
s.charge_acquisition_cutoff_at = cutoff
|
||||
return slots
|
||||
|
||||
def _home01_battery(self, soc: float = 30_000.0) -> SimpleNamespace:
|
||||
b = _battery(
|
||||
uc_wh=64_000.0,
|
||||
min_pct=11.0,
|
||||
arb_pct=20.0,
|
||||
terminal_soc_value_factor=0.2,
|
||||
)
|
||||
b.max_charge_power_w = 17_000
|
||||
b.max_discharge_power_w = 17_000
|
||||
b.charge_slot_buffer = 1.3
|
||||
b.planner_daytime_charge_target_enabled = True
|
||||
return b
|
||||
|
||||
def _home01_grid(self) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
max_import_power_w=17_000,
|
||||
max_export_power_w=13_500,
|
||||
block_export_on_negative_sell=False,
|
||||
purchase_pricing_mode="spot",
|
||||
)
|
||||
|
||||
def test_home01_no_night_charge_before_pv_day(self) -> None:
|
||||
"""Pattern run 16522: 22:00-24:00 bez grid importu >15 kW pred PV dnem."""
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
slots = self._home01_run16522_slots()
|
||||
results, _snap = self._solve_auto(
|
||||
slots,
|
||||
self._home01_battery(),
|
||||
30_000.0,
|
||||
)
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
for r in results:
|
||||
h = r.interval_start.astimezone(prague).hour
|
||||
if h in (22, 23):
|
||||
self.assertLess(
|
||||
r.grid_setpoint_w,
|
||||
15_000,
|
||||
f"slot {r.interval_start}: grid={r.grid_setpoint_w} >= 15 kW",
|
||||
)
|
||||
|
||||
def test_two_pass_converged_after_filter(self) -> None:
|
||||
"""Po self-konzistentni masce B: acquisition pass1 ~ pass2."""
|
||||
slots = self._home01_run16522_slots()
|
||||
_results, snap = self._solve_auto(slots, self._home01_battery(), 30_000.0)
|
||||
inputs = snap.get("inputs") or {}
|
||||
self.assertTrue(
|
||||
inputs.get("two_pass_converged"),
|
||||
f"acquisition diverguje: {inputs}",
|
||||
)
|
||||
|
||||
|
||||
class LoadFirstDispatchTests(unittest.TestCase):
|
||||
"""Deye load-first: PV do spotřeby dřív než bc_pv/ge_pv z přebytku."""
|
||||
|
||||
Reference in New Issue
Block a user