a dalsi fix
This commit is contained in:
@@ -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:00–14: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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user