implementace dynamickeho bodu T (kde se rodpojuje PV A)
Some checks failed
CI and deploy / migration-check (push) Failing after 15s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-26 13:28:31 +02:00
parent a53bcd0b81
commit 58b0a2f882
6 changed files with 310 additions and 73 deletions

View File

@@ -3703,7 +3703,7 @@ class PlannerArbitrageImprovementsTests(unittest.TestCase):
class NegSellSocPhaseTests(unittest.TestCase):
"""Fázované SoC v okně sell<0 (v32): prep 80 %, tail rampa, vent B s prahem."""
"""Fázované SoC v okně sell<0 (v35): rampa z PV B, tail, vent B s prahem."""
@staticmethod
def _phase_battery(**kw: float) -> SimpleNamespace:
@@ -3750,11 +3750,26 @@ class NegSellSocPhaseTests(unittest.TestCase):
def test_day_phases_tail_last_four(self) -> None:
slots = self._neg_sell_slots(10)
bat = self._phase_battery(tail_slots=4)
phases, targets, _w = _neg_sell_day_phases(slots, bat)
phases, targets, _w, meta = _neg_sell_day_phases(slots, bat)
self.assertEqual(phases[5], "prep")
self.assertEqual(phases[9], "tail")
self.assertEqual(phases.count("tail"), 4)
self.assertAlmostEqual(float(targets[9] or 0), bat.soc_max_wh, delta=50.0)
self.assertTrue(meta.get("neg_sell_b_ramp_v35"))
prep_targets = [float(targets[t] or 0) for t in range(6) if phases[t] == "prep"]
self.assertGreater(len(prep_targets), 1)
for a, b in zip(prep_targets, prep_targets[1:]):
self.assertGreaterEqual(b, a - 1.0)
def test_b_ramp_t_detach_and_surplus_meta(self) -> None:
slots = self._neg_sell_slots(12, pv_b=6000)
bat = self._phase_battery(tail_slots=4)
_ph, _tg, _w, meta = _neg_sell_day_phases(slots, bat)
self.assertIsNotNone(meta.get("t_detach_idx"))
self.assertGreaterEqual(int(meta["t_detach_idx"]), 0)
self.assertLess(int(meta["t_detach_idx"]), 8)
self.assertGreater(float(meta.get("e_surplus_after_t_wh") or 0), 0.0)
self.assertIn("post_detach_prep_ts", meta)
def test_prep_reaches_soc_by_mid_window(self) -> None:
slots = self._neg_sell_slots(12)
@@ -3782,6 +3797,8 @@ class NegSellSocPhaseTests(unittest.TestCase):
)
self.assertEqual(snap.get("planner_build_tag"), PLANNER_BUILD_TAG)
self.assertTrue(snap.get("inputs", {}).get("neg_sell_phases_enabled"))
self.assertTrue(snap.get("inputs", {}).get("neg_sell_b_ramp_v35"))
self.assertIsNotNone(snap.get("inputs", {}).get("t_detach_idx"))
# Nabíjení z FVE v sell<0: SoC roste, tail má vyšší cíl než začátek okna.
self.assertGreater(results[-1].battery_soc_target, results[0].battery_soc_target)
self.assertGreaterEqual(results[-1].battery_soc_target, 75.0)
@@ -3875,7 +3892,7 @@ class NegSellSocPhaseTests(unittest.TestCase):
class PreNegPvExportForecastTests(unittest.TestCase):
"""v33: export FVE před sell<0 jen pokud forecast v sell<0 okně pokryje prep SoC."""
"""v33/v35: export FVE před sell<0 jen pokud forecast B v sell<0 okně pokryje soc_need z rampy."""
@staticmethod
def _slots_morning_then_neg(n: int = 22, *, neg_pv_scale: float = 1.0) -> list[PlanningSlot]:
@@ -3885,7 +3902,8 @@ class PreNegPvExportForecastTests(unittest.TestCase):
sell = -0.25 if i >= 6 else (2.8 if i < 4 else 1.2)
if i >= 6:
pv_a = (8000 + (i - 6) * 500) * neg_pv_scale
pv_b = 6000.0 * neg_pv_scale
# v35 cushion: usable jen z B — dostatečný B pro rampu v test_cushion_ok
pv_b = 9500.0 * neg_pv_scale
else:
pv_a = 1500 + i * 400
pv_b = 1500.0