a dalsi fix
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-25 00:10:58 +02:00
parent b844a9182f
commit 9ba65ea6bb
5 changed files with 170 additions and 8 deletions

View File

@@ -1010,5 +1010,125 @@ class FixedPurchasePricingTests(unittest.TestCase):
self.assertIn(2, discharge, "oba sloty sell > buy + degrad")
class NegSellReservationTests(unittest.TestCase):
"""Mirror SQL `R__063` PV vrstva A — rezervace `v_pv_layer_cap_wh` pro `sell<0` okno.
Cíl: do prvního `sell<0` slotu dorazit s SoC = soc_max min(neg_window_pv, available_storage),
aby `sell<0` PV mohlo doplnit zbytek bez exportu / curtail pole A.
"""
@staticmethod
def _pv_layer_cap_with_reservation(
slots, # list of dict { sell, pv_surplus_w }
energy_to_fill_wh: float,
grid_filled_wh: float,
max_charge_w: float,
charge_eff: float,
) -> float:
cap = max(energy_to_fill_wh - grid_filled_wh, 0.0)
neg_window = sum(
min(float(s["pv_surplus_w"]), max_charge_w) * charge_eff * 0.25
for s in slots
if float(s["sell"]) < 0 and float(s["pv_surplus_w"]) > 0
)
return max(cap - neg_window, 0.0)
def test_neg_sell_window_reduces_pv_layer_cap(self) -> None:
# 4 ranní PV sloty (sell>=0), 2 odpolední neg-sell PV sloty
slots = [
{"sell": 1.2, "pv_surplus_w": 6000}, # 10:00 cap candidate
{"sell": 1.0, "pv_surplus_w": 7000}, # 11:00
{"sell": 0.8, "pv_surplus_w": 8000}, # 12:00
{"sell": -0.5, "pv_surplus_w": 9000}, # 13:00 neg-sell
{"sell": -1.0, "pv_surplus_w": 9000}, # 13:30 neg-sell
]
cap_before = 20_000.0 # 20 kWh deficit
cap_after = self._pv_layer_cap_with_reservation(
slots,
energy_to_fill_wh=cap_before,
grid_filled_wh=0.0,
max_charge_w=10_000.0,
charge_eff=0.95,
)
# neg-window = 2 × min(9000, 10000) × 0.95 × 0.25 = 4 275
self.assertAlmostEqual(cap_after, cap_before - 4275.0, places=1)
self.assertLess(cap_after, cap_before)
def test_neg_sell_pv_covers_full_deficit(self) -> None:
slots = [
{"sell": 1.0, "pv_surplus_w": 5000}, # ranní
{"sell": -0.4, "pv_surplus_w": 9000},
{"sell": -1.1, "pv_surplus_w": 9000},
{"sell": -0.8, "pv_surplus_w": 9000},
]
# neg-window = 3 × 9000 × 0.95 × 0.25 = 6 412.5 Wh
cap_after = self._pv_layer_cap_with_reservation(
slots,
energy_to_fill_wh=5_000.0,
grid_filled_wh=0.0,
max_charge_w=10_000.0,
charge_eff=0.95,
)
# cap (5000) - neg_window (6412.5) → clamp na 0 → žádné ranní PV nabíjení
self.assertEqual(cap_after, 0.0)
def test_no_neg_sell_window_no_change(self) -> None:
slots = [
{"sell": 1.2, "pv_surplus_w": 6000},
{"sell": 0.8, "pv_surplus_w": 7000},
]
cap_before = 8_000.0
cap_after = self._pv_layer_cap_with_reservation(
slots,
energy_to_fill_wh=cap_before,
grid_filled_wh=0.0,
max_charge_w=10_000.0,
charge_eff=0.95,
)
self.assertEqual(cap_after, cap_before)
class FallbackAcquisitionTests(unittest.TestCase):
"""Mirror SQL `R__063` fallback when `v_est_grid_wh = 0` and no positive buy grid slot."""
@staticmethod
def _fallback_acq(slots, est_grid_wh: float, est_grid_cost: float) -> float:
if est_grid_wh > 0:
return est_grid_cost / est_grid_wh
positive_buys = [float(s["buy"]) for s in slots if float(s["buy"]) >= 0]
v = min(positive_buys) if positive_buys else 0.0
return max(v, 0.0)
def test_no_grid_charging_but_horizon_has_positive_buys(self) -> None:
# PM má negativní buy (13:0014:00), ale jinde v horizontu pozitivní min
slots = [
{"buy": 4.5, "hour": 23},
{"buy": 0.3, "hour": 7},
{"buy": 0.7, "hour": 10},
{"buy": -0.36, "hour": 13},
{"buy": -0.43, "hour": 14},
]
acq = self._fallback_acq(slots, est_grid_wh=0.0, est_grid_cost=0.0)
self.assertAlmostEqual(acq, 0.3, places=4)
self.assertGreaterEqual(acq, 0.0)
def test_all_negative_buys_clamps_to_zero(self) -> None:
slots = [
{"buy": -0.4, "hour": 13},
{"buy": -0.5, "hour": 14},
]
acq = self._fallback_acq(slots, est_grid_wh=0.0, est_grid_cost=0.0)
self.assertEqual(acq, 0.0)
def test_positive_weighted_mean_unchanged(self) -> None:
# est_grid_wh > 0 — vrátit weighted mean, ne fallback
acq = self._fallback_acq(
[{"buy": 0.7, "hour": 10}],
est_grid_wh=4000.0,
est_grid_cost=4000.0 * 0.7,
)
self.assertAlmostEqual(acq, 0.7, places=4)
if __name__ == "__main__":
unittest.main()

View File

@@ -1230,7 +1230,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
50.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
self.assertGreater(
results[0].battery_setpoint_w,
5_500,
@@ -1380,7 +1380,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
50.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
self.assertEqual(len(results), len(slots))
def test_gen_cutoff_full_soc_neg_sell_with_pv_b_feasible(self) -> None:
@@ -1444,7 +1444,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
55.0,
operating_mode="AUTO",
)
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-self-consistent-grid-mask-v12")
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
self.assertEqual(len(results), len(slots))
def test_fixed_tariff_neg_sell_no_grid_export(self) -> None: