diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index d04e933..7f694f5 100644 --- a/backend/services/planning_engine.py +++ b/backend/services/planning_engine.py @@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0 PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0 PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0 PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0 -PLANNER_BUILD_TAG = "2026-05-31-evening-push-relaxed-clear-v54" +PLANNER_BUILD_TAG = "2026-05-31-evening-push-any-relaxed-v55" # Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak). DAWN_LOW_PV_NO_CURTAIL_W = 1500 # Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu). @@ -234,20 +234,29 @@ def _maybe_add_planner_comparison( peer_version = _planner_peer_version(active_version) if peer_version == active_version: return None - peer_results, peer_ms, peer_snapshot = solve_dispatch( - slots, - battery, - heat_pump, - grid, - ev_sessions, - vehicles, - current_soc_wh, - current_tuv_temp_c, - tuv_delta_stats=tuv_delta_stats, - operating_mode=operating_mode, - charge_commitment_prev_w=charge_commitment_prev_w, - planner_version=peer_version, - ) + try: + peer_results, peer_ms, peer_snapshot = solve_dispatch_two_pass( + slots, + battery, + heat_pump, + grid, + ev_sessions, + vehicles, + current_soc_wh, + current_tuv_temp_c, + tuv_delta_stats=tuv_delta_stats, + operating_mode=operating_mode, + charge_commitment_prev_w=charge_commitment_prev_w, + planner_version=peer_version, + evening_push_ts_override=None, + ) + except RuntimeError as exc: + logger.warning( + "Planner comparison peer (%s) failed, skipping compare run: %s", + peer_version, + exc, + ) + return None # active_results / active_ms jsou doplněny později v calleru return { "peer_version": peer_version, @@ -2721,7 +2730,12 @@ def solve_dispatch( evening_push_hysteresis_retained = True else: evening_push_ts = computed_evening_push_ts - if relaxed_neg_prep_window: + if ( + relaxed_expensive_import + or relaxed_neg_buy_charge + or relaxed_neg_prep_window + or neg_sell_phases_fallback + ): evening_push_ts = set() evening_push_hysteresis_retained = False last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy( @@ -2843,9 +2857,16 @@ def solve_dispatch( commit_pen = float(getattr(battery, "planner_charge_commitment_penalty_czk_kwh", 0.2)) commit_lp: list[tuple[int, pulp.LpVariable, float]] = [] - if charge_commitment_prev_w is not None and len(charge_commitment_prev_w) == T: + commitment_for_solve = charge_commitment_prev_w + if ( + relaxed_neg_buy_charge + or relaxed_neg_prep_window + or neg_sell_phases_fallback + ): + commitment_for_solve = None + if commitment_for_solve is not None and len(commitment_for_solve) == T: for t in range(T): - prev = charge_commitment_prev_w[t] + prev = commitment_for_solve[t] if prev is not None and prev > 500: cap_prev = float(prev) cv = pulp.LpVariable(f"ccommit_{t}", 0, cap_prev) @@ -4097,6 +4118,7 @@ def solve_dispatch( planner_version=planner_version, relaxed_expensive_import=True, relaxed_neg_buy_charge=True, + evening_push_ts_override=evening_push_ts_override, ) if not relaxed_neg_prep_window: logger.warning( @@ -4503,7 +4525,15 @@ def solve_dispatch( "evening_push_override_filtered_empty": bool( push_override_raw and not push_override_eff ), - "evening_push_cleared_on_relaxed_prep": bool(relaxed_neg_prep_window), + "evening_push_cleared_on_relaxed_prep": bool( + relaxed_neg_prep_window + or relaxed_neg_buy_charge + or relaxed_expensive_import + or neg_sell_phases_fallback + ), + "charge_commitment_ignored_on_relaxed": bool( + commitment_for_solve is None and charge_commitment_prev_w is not None + ), "kv1_evening_push_morning_peak_rule": _kv1_block_export_fixed_evening_push( grid, purchase_fixed=purchase_fixed_pre, diff --git a/backend/tests/test_planning_dispatch_milp.py b/backend/tests/test_planning_dispatch_milp.py index 09fb17d..f79bd09 100644 --- a/backend/tests/test_planning_dispatch_milp.py +++ b/backend/tests/test_planning_dispatch_milp.py @@ -2897,6 +2897,46 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase): self.assertNotIn(0, out) self.assertIn(1, out) + def test_relaxed_expensive_import_clears_evening_push(self) -> None: + """v55: už 1. retry (relaxed_expensive_import) vypne tvrdý evening push.""" + prague = ZoneInfo("Europe/Prague") + slots = [ + PlanningSlot( + interval_start=datetime(2026, 5, 30, 22, 0, tzinfo=prague).astimezone(timezone.utc), + buy_price=3.0, + sell_price=6.0, + pv_a_forecast_w=0, + pv_b_forecast_w=0, + load_baseline_w=500, + ev1_connected=False, + ev2_connected=False, + allow_discharge_export=True, + ), + ] + battery = _battery(uc_wh=64_000.0) + battery.max_discharge_power_w = 18_000 + grid = SimpleNamespace( + max_export_power_w=13_500, + max_import_power_w=17_000, + block_export_on_negative_sell=False, + purchase_pricing_mode="spot", + ) + _results, _ms, snap = solve_dispatch( + slots, + battery, + SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0), + grid, + [None, None], + [ + SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0), + SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0), + ], + current_soc_wh=16_000.0, + current_tuv_temp_c=55.0, + relaxed_expensive_import=True, + ) + self.assertEqual(snap["inputs"].get("evening_push_ts"), []) + def test_relaxed_neg_prep_clears_computed_evening_push(self) -> None: """v54: relaxed_neg_prep_window vypne tvrdý ge_bat push (ne jen hysterézní override).""" prague = ZoneInfo("Europe/Prague") diff --git a/docs/04-modules/planning.md b/docs/04-modules/planning.md index 8a4e9ba..ac2ab56 100644 --- a/docs/04-modules/planning.md +++ b/docs/04-modules/planning.md @@ -125,9 +125,11 @@ flowchart TD 8. **v53 — rolling hysteréze push:** při Infeasible retry se **`evening_push_ts_override` zahodí**; filtr override slotů (export maska, bez defer PV). Snap: `evening_push_override_dropped_on_retry`. Tag **`2026-05-31-evening-push-override-retry-v53`**. -9. **v54 — relaxed prep + two-pass:** při **`relaxed_neg_prep_window`** i vypočtený **`evening_push_ts = ∅`** (tvrdý push off); pass2 two-pass **nepoužívá override** a dědí relax vlajky z pass1. Snap: `evening_push_cleared_on_relaxed_prep`. Tag **`2026-05-31-evening-push-relaxed-clear-v54`**. +9. **v54 — relaxed prep + two-pass:** při **`relaxed_neg_prep_window`** i vypočtený **`evening_push_ts = ∅`**; pass2 two-pass **nepoužívá override** a dědí relax vlajky z pass1. Tag **`2026-05-31-evening-push-relaxed-clear-v54`**. -**Funkce:** … Tag: **`2026-05-30-post-push-night-battery-v47`** (spot); KV1 v52 / home-01 v54 viz výše. +10. **v55 — jakýkoli relaxed retry:** tvrdý push off už od **`relaxed_expensive_import`**; commitment z minulého plánu ignorovat od **`relaxed_neg_buy_charge`**; comparison v2 **non-fatal**. Tag **`2026-05-31-evening-push-any-relaxed-v55`**. + +**Funkce:** … Tag: **`2026-05-30-post-push-night-battery-v47`** (spot); KV1 v52 / home-01 v55 viz výše. ### Arbitráž baterie — účtování mezi sloty (povinné čtení) diff --git a/docs/planning-changelog.md b/docs/planning-changelog.md index c4e92a9..1150598 100644 --- a/docs/planning-changelog.md +++ b/docs/planning-changelog.md @@ -5,6 +5,16 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen --- +## 2026-05-31 — home-01: evening push při každém relaxed retry (v55) + +**Problém:** v54 maže tvrdý push až u `relaxed_neg_prep_window` (3. retry). Retry 1–2 pořád držely **vypočtený** `evening_push_ts` → u ~25 % SoC často stále **Infeasible**. Ruční „Přeplánovat“ navíc spadlo, když **v2 comparison** peer selhal (active v1 prošel). V DB po pádu **žádný `api` run** — scheduler v51/v54 mezitím OK. + +**Změna (v55):** tvrdý `evening_push_ts = ∅` při **jakékoli** relaxed vlajce; rolling commitment ignorovat od `relaxed_neg_buy_charge`; comparison peer = `solve_dispatch_two_pass` + **non-fatal** skip. Snap: `evening_push_cleared_on_relaxed_prep`, `charge_commitment_ignored_on_relaxed`. + +Tag **`2026-05-31-evening-push-any-relaxed-v55`**. + +--- + ## 2026-05-31 — home-01: tvrdý evening push po relaxed prep (v54) **Problém:** v53 maže jen **hysterézní override**, ne **vypočtený** `evening_push_ts`. Po `relaxed_neg_prep_window` (typicky home-01 ~25 % SoC + neg den 31.5.) zůstávaly tvrdé `ge_bat`/`z_export` v push slotech → **`Solver: Infeasible`** i po celém retry řetězci. Pass2 two-pass znovu aplikoval override bez carryover relaxace.