From 09bca0a9034a3d1d4aa320a130d41030c108791f Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Sat, 6 Jun 2026 22:28:48 +0200 Subject: [PATCH] =?UTF-8?q?Branch=202:=20home-01=20neg-ve=C4=8Der=20?= =?UTF-8?q?=E2=80=94=20export=20k=20reserve=5Fsoc,=20fix=20pos=5Fsell=5Fpr?= =?UTF-8?q?e=5Fneg=5Fbuy=20+=20odd=C4=9Blit=20evening=5Fpush=20od=20prep?= =?UTF-8?q?=20relax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/services/planning_engine.py | 166 +++++++++++++++++-- backend/tests/test_planning_dispatch_milp.py | 93 ++++++++++- docs/04-modules/planning.md | 11 +- docs/planning-changelog.md | 18 ++ 4 files changed, 267 insertions(+), 21 deletions(-) diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index eb4c506..48d6f75 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-06-06-infeasible-journal-granular-prep-relax-v63" +PLANNER_BUILD_TAG = "2026-06-06-future-neg-buy-evening-export-v64" SOLVER_RELAX_STEPS: tuple[str, ...] = ( "strict", "relaxed_expensive_import", @@ -93,6 +93,8 @@ NEG_EVENING_PREP_DISCHARGE_SHORTFALL_PENALTY_CZK_KWH = 120.0 # Kotva: SoC na konci večera D−1 a těsně před 1. sell<0 ráno D ≤ reserve_soc. NEG_EVENING_RESERVE_SOC_MAX_SLACK_WH = 400.0 NEG_EVENING_RESERVE_SOC_SLACK_PENALTY_CZK_PER_WH = 55.0 +# Terminal SoC shadow price: při blízkém buy<0 nesmí LP „šetřit“ baterii ve večerní špičce. +FUTURE_NEG_BUY_TERMINAL_SOC_FACTOR_MULT = 0.1 # Před prvním sell<0: export FVE jen pokud predikce v sell<0 okně pokryje dobítí na prep cíl. PRE_NEG_PV_EXPORT_FORECAST_MARGIN = 1.15 PRE_NEG_PV_EXPORT_MIN_NEEDED_WH = 2500.0 @@ -1369,6 +1371,100 @@ def _neg_evening_discharge_budget_wh( ) +def _first_neg_sell_idx_on_prague_day( + slots: list[PlanningSlot], + prague_day: object, +) -> int | None: + for t, st in enumerate(slots): + if _prague_calendar_date(st) != prague_day: + continue + if float(st.sell_price) < 0.0: + return t + return None + + +def _future_neg_buy_discharge_enabled( + slots: list[PlanningSlot], + battery: Any, + *, + first_neg_buy_idx: int, + first_neg_sell_idx: int | None, + observed_soc_wh: float, + neg_sell_phases_en: bool, + neg_sell_soc_target_by_t: list[Optional[float]] | None = None, +) -> bool: + """ + Večerní vývoz k reserve_soc před dnem s buy<0: aktivní i při relaxed_neg_prep_window, + pokud FVE v sell<0 okně pokryje deficit do prep rampy (× PRE_NEG_PV_EXPORT_FORECAST_MARGIN). + """ + if first_neg_buy_idx <= 0: + return False + neg_buy_day = _prague_calendar_date(slots[first_neg_buy_idx]) + neg_sell_t = first_neg_sell_idx + if ( + neg_sell_t is None + or _prague_calendar_date(slots[neg_sell_t]) != neg_buy_day + ): + neg_sell_t = _first_neg_sell_idx_on_prague_day(slots, neg_buy_day) + if neg_sell_t is None: + return False + if neg_sell_phases_en and neg_sell_soc_target_by_t is not None: + tgt = neg_sell_soc_target_by_t[neg_sell_t] + target_wh = float(tgt) if tgt is not None else float(battery.soc_max_wh) + else: + target_wh = float(battery.soc_max_wh) + reserve_wh = float( + getattr(battery, "reserve_soc_wh", getattr(battery, "min_soc_wh", 0.0)) + ) + soc_obs = max( + float(battery.min_soc_wh), + min(float(observed_soc_wh), float(battery.soc_max_wh)), + ) + if soc_obs <= reserve_wh + 1e-3: + return False + if soc_obs >= target_wh - 1e-3: + return True + usable_wh = _neg_sell_day_pv_usable_wh( + slots, + neg_sell_t, + max_charge_power_w=float(battery.max_charge_power_w), + charge_efficiency=float(battery.charge_efficiency), + ) + needed_wh = max(0.0, target_wh - soc_obs) + if needed_wh < PRE_NEG_PV_EXPORT_MIN_NEEDED_WH: + return True + return usable_wh >= needed_wh * PRE_NEG_PV_EXPORT_FORECAST_MARGIN + + +def _pos_sell_pre_neg_buy_evening_export_exempt_ts( + slots: list[PlanningSlot], + pos_sell_pre_neg_buy_ts: list[int], + evening_peak_export_ts: list[int], + *, + charge_acquisition_czk_kwh: float, + min_spread: float, + fixed_tariff: bool, + future_neg_buy_discharge_en: bool, +) -> set[int]: + """Večerní peak před buy<0: neaplikovat ge=0, pokud je vývoz ekonomicky výhodný.""" + if not future_neg_buy_discharge_en: + return set() + evening_peak_set = set(evening_peak_export_ts) + out: set[int] = set() + for t in pos_sell_pre_neg_buy_ts: + if t not in evening_peak_set and not _in_evening_push_hour_window(slots[t]): + continue + if not _slot_profitable_battery_export( + slots[t], + charge_acquisition_czk_kwh=charge_acquisition_czk_kwh, + min_spread=min_spread, + fixed_tariff=fixed_tariff, + ): + continue + out.add(t) + return out + + def _neg_evening_before_neg_push_indices( slots: list[PlanningSlot], candidate_ts: set[int], @@ -2386,7 +2482,7 @@ def solve_dispatch( relaxed_expensive_import: nouzový režim po Infeasible — síť smí krmit baseload v drahých slotech. relaxed_neg_buy_charge: druhý nouzový retry bez neg_buy charge shortfall. relaxed_neg_prep_hold_only: třetí retry — bez prep_soc_shortfall a prep hold binárek (evening push zůstává). - relaxed_neg_prep_window: čtvrtý retry — navíc vypne neg-evening bundle a tvrdý evening push. + relaxed_neg_prep_window: čtvrtý retry — vypne strict pre-neg bundle; future_neg_buy večerní export zůstává. """ T = len(slots) if T < 1: @@ -2577,11 +2673,7 @@ def solve_dispatch( if horizon_slots_h24 > 0 else 4.0 ) - terminal_factor = float(battery.planner_terminal_soc_value_factor) - # Kč/Wh: ocenění energie ponechané v baterii na konci horizontu (receding horizon kotva). - terminal_soc_kcz_per_wh = ( - avg_buy_terminal * terminal_factor / 1000.0 - ) + terminal_factor_base = float(battery.planner_terminal_soc_value_factor) charge_acq_raw = getattr(slots[0], "charge_acquisition_buy_czk_kwh", None) charge_acquisition_czk_kwh = ( @@ -2676,12 +2768,38 @@ def solve_dispatch( neg_evening_push_ts: set[int] = set() neg_evening_export_budget_wh: float | None = None neg_evening_reserve_anchors: list[tuple[int, float]] = [] + future_neg_buy_discharge_en = False if ( + om == "AUTO" + and not purchase_fixed_pre + and first_neg_buy_idx is not None + and first_neg_buy_idx > 0 + ): + future_neg_buy_discharge_en = _future_neg_buy_discharge_enabled( + slots, + battery, + first_neg_buy_idx=first_neg_buy_idx, + first_neg_sell_idx=first_neg_sell_idx, + observed_soc_wh=observed_soc_wh, + neg_sell_phases_en=neg_sell_phases_en, + neg_sell_soc_target_by_t=( + neg_sell_soc_target_by_t if neg_sell_phases_en else None + ), + ) + terminal_factor = terminal_factor_base + if future_neg_buy_discharge_en: + terminal_factor *= FUTURE_NEG_BUY_TERMINAL_SOC_FACTOR_MULT + # Kč/Wh: ocenění energie ponechané v baterii na konci horizontu (receding horizon kotva). + terminal_soc_kcz_per_wh = avg_buy_terminal * terminal_factor / 1000.0 + + neg_evening_bundle_strict = ( om == "AUTO" and not purchase_fixed_pre and neg_sell_phases_en and not relaxed_neg_prep_window - ): + ) + neg_evening_discharge_active = neg_evening_bundle_strict or future_neg_buy_discharge_en + if neg_evening_bundle_strict: pre_neg_pv_export_ts, pre_neg_cushion_by_day = _pre_neg_pv_export_bundle( slots, battery, @@ -2690,9 +2808,13 @@ def solve_dispatch( neg_sell_phases_en=True, soc_target_by_t=neg_sell_soc_target_by_t, ) + if neg_evening_discharge_active: + meta_for_evening = neg_sell_day_meta + if not (meta_for_evening.get("days")) and first_neg_sell_idx is not None: + meta_for_evening = {"days": [{"first_neg_idx": first_neg_sell_idx}]} neg_evening_before_neg_ts = _evening_discharge_before_neg_day_ts( slots, - neg_sell_day_meta, + meta_for_evening, ) neg_evening_before_neg_ts |= _discharge_before_first_neg_sell_ts( slots, @@ -2700,7 +2822,7 @@ def solve_dispatch( ) neg_evening_reserve_anchors = _neg_evening_reserve_soc_anchors( slots, - neg_sell_day_meta, + meta_for_evening, battery, ) reserve_wh = float( @@ -2856,10 +2978,8 @@ def solve_dispatch( first_neg_sell_idx=first_neg_sell_idx, kv1_evening_push=kv1_evening_push_pre, ) - # Tvrdý ge_bat push vypnout jen v prep/fallback retry (ne při rei — jinak zmizí vývoz v špičce). - evening_push_hard_suppressed = bool( - relaxed_neg_prep_window or neg_sell_phases_fallback - ) + # Tvrdý ge_bat push vypnout jen při neg_sell fallback (ne při prep relax — v64). + evening_push_hard_suppressed = bool(neg_sell_phases_fallback) else: evening_push_hard_suppressed = False last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy( @@ -2868,6 +2988,15 @@ def solve_dispatch( pos_sell_pre_neg_buy_ts = _positive_sell_pre_neg_buy_indices( slots, first_neg_buy_idx ) + pos_sell_pre_neg_buy_ge_exempt_ts = _pos_sell_pre_neg_buy_evening_export_exempt_ts( + slots, + pos_sell_pre_neg_buy_ts, + evening_peak_export_ts, + charge_acquisition_czk_kwh=charge_acquisition_czk_kwh, + min_spread=float(degradation_cost_effective), + fixed_tariff=fixed_tariff_like_pre, + future_neg_buy_discharge_en=future_neg_buy_discharge_en, + ) pre_neg_buy_empty_ts = _pre_neg_buy_empty_discharge_indices( slots, first_neg_buy_idx, last_pos_sell_pre_neg_buy ) @@ -3980,6 +4109,7 @@ def solve_dispatch( first_neg_buy_idx is not None and first_neg_buy_idx > 0 and t in pos_sell_pre_neg_buy_ts + and t not in pos_sell_pre_neg_buy_ge_exempt_ts ): prob += ge[t] == 0 prob += ge_pv[t] == 0 @@ -4332,7 +4462,7 @@ def solve_dispatch( if not relaxed_neg_prep_window: logger.warning( "solve_dispatch still Infeasible, retry with relaxed_neg_prep_window " - "(skip neg-evening bundle and tvrdý evening push)" + "(skip strict pre-neg bundle; future_neg_buy evening export kept)" ) return solve_dispatch( slots, @@ -4754,6 +4884,12 @@ def solve_dispatch( push_override_raw and not push_override_eff ), "evening_push_hard_suppressed": bool(evening_push_hard_suppressed), + "future_neg_buy_discharge": bool(future_neg_buy_discharge_en), + "terminal_soc_factor_effective": float(terminal_factor), + "pos_sell_pre_neg_buy_ge_exempt_slots": [ + slots[i].interval_start.isoformat() + for i in sorted(pos_sell_pre_neg_buy_ge_exempt_ts) + ], "evening_push_peak_fallback_used": bool( om == "AUTO" and not computed_evening_push_ts diff --git a/backend/tests/test_planning_dispatch_milp.py b/backend/tests/test_planning_dispatch_milp.py index 9f48785..784b774 100644 --- a/backend/tests/test_planning_dispatch_milp.py +++ b/backend/tests/test_planning_dispatch_milp.py @@ -3326,8 +3326,8 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase): self.assertFalse(snap["inputs"].get("evening_push_hard_suppressed")) self.assertLess(results[0].grid_setpoint_w, -1000) - def test_relaxed_neg_prep_suppresses_hard_push_only(self) -> None: - """v57: relaxed_neg_prep_window vypne jen tvrdý push, ne seznam slotů.""" + def test_relaxed_neg_prep_keeps_hard_push_v64(self) -> None: + """v64: relaxed_neg_prep_window nesmí vypnout tvrdý evening push (jen fallback).""" prague = ZoneInfo("Europe/Prague") slots = [ PlanningSlot( @@ -3377,7 +3377,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase): current_tuv_temp_c=55.0, relaxed_neg_prep_window=True, ) - self.assertTrue(snap["inputs"].get("evening_push_hard_suppressed")) + self.assertFalse(snap["inputs"].get("evening_push_hard_suppressed")) push_iso = snap["inputs"].get("evening_push_ts") or [] self.assertGreaterEqual(len(push_iso), 1) @@ -3458,6 +3458,93 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase): ], ) + def test_future_neg_buy_evening_export_at_high_soc_relaxed_prep(self) -> None: + """v64: před buy<0 večerní export i při relaxed_neg_prep_window (neg-evening bundle).""" + prague = ZoneInfo("Europe/Prague") + base = datetime(2026, 6, 6, 19, 0, tzinfo=prague).astimezone(timezone.utc) + slots: list[PlanningSlot] = [] + for i in range(96): + local = (base + timedelta(minutes=15 * i)).astimezone(prague) + d, h, m = local.day, local.hour, local.minute + hm = h + m / 60.0 + if d == 6: + buy, sell = 3.0, 5.3 + pv_a, pv_b = 0, 0 + else: + sell = -0.2 if hm >= 5.75 and hm < 15 else 2.5 + buy = -0.5 if 11 <= hm < 14 else 2.0 + pv_a = 6000 if 8 <= h < 16 else 200 + pv_b = 8000 if 8 <= h < 16 else 200 + slots.append( + PlanningSlot( + interval_start=base + timedelta(minutes=15 * i), + buy_price=buy, + sell_price=sell, + pv_a_forecast_w=pv_a, + pv_b_forecast_w=pv_b, + load_baseline_w=500, + ev1_connected=False, + ev2_connected=False, + allow_charge=True, + allow_discharge_export=True, + charge_acquisition_buy_czk_kwh=0.8, + ) + ) + bat = _battery(uc_wh=64_000.0, arb_pct=20.0, terminal_soc_value_factor=0.0) + bat.max_discharge_power_w = 18_000 + bat.max_charge_power_w = 18_000 + bat.planner_neg_sell_prep_soc_percent = 80 + bat.planner_neg_sell_full_soc_tail_slots = 4 + hp = SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0) + grid = SimpleNamespace( + max_import_power_w=17_000, + max_export_power_w=13_500, + block_export_on_negative_sell=False, + purchase_pricing_mode="spot", + ) + vehicles = [ + 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), + ] + relax_kw = dict( + relaxed_expensive_import=True, + relaxed_neg_buy_charge=True, + relaxed_neg_prep_hold_only=True, + relaxed_neg_prep_window=True, + ) + results_hi, _ms, snap_hi = solve_dispatch( + slots, + bat, + hp, + grid, + [None, None], + vehicles, + 0.81 * bat.soc_max_wh, + 50.0, + operating_mode="AUTO", + **relax_kw, + ) + self.assertTrue(snap_hi["inputs"].get("future_neg_buy_discharge")) + self.assertGreater(len(snap_hi["inputs"].get("neg_evening_push_slots") or []), 0) + self.assertLess(results_hi[0].grid_setpoint_w, -1000) + self.assertLess(results_hi[0].battery_soc_target, 80.0) + + results_mid, _ms2, snap_mid = solve_dispatch( + slots, + bat, + hp, + grid, + [None, None], + vehicles, + 0.35 * bat.soc_max_wh, + 50.0, + operating_mode="AUTO", + **relax_kw, + ) + self.assertFalse(snap_mid["inputs"].get("evening_push_hard_suppressed")) + self.assertFalse(snap_mid["inputs"].get("neg_sell_phases_fallback")) + self.assertLess(results_mid[0].grid_setpoint_w, -1000) + def test_kv1_evening_push_profitable_vs_morning_zone_peak(self) -> None: """v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne.""" prague = ZoneInfo("Europe/Prague") diff --git a/docs/04-modules/planning.md b/docs/04-modules/planning.md index 45633f0..e23964e 100644 --- a/docs/04-modules/planning.md +++ b/docs/04-modules/planning.md @@ -132,7 +132,7 @@ flowchart TD 11. **v56 — ranní tvrdý export:** `morning_pre_neg_export` / pre-neg discharge **jen strict**; pass2 Infeasible → **pass1**. Tag **`2026-05-31-morning-export-relaxed-v56`**. -12. **v57 — večerní push po rei:** `relaxed_expensive_import` **nesmí** vymazat `evening_push_ts`; tvrdý `ge_bat` push vypnut jen při `relaxed_neg_prep_window`. Tag **`2026-06-01-evening-push-keep-on-relaxed-import-v57`**. +12. **v57 / v64 — večerní push po rei / prep relax:** `relaxed_expensive_import` **nesmí** vymazat `evening_push_ts`; tvrdý `ge_bat` push vypnut jen při **`neg_sell_phases_fallback`** (v64: ne při `relaxed_neg_prep_window`). Tag **`2026-06-01-evening-push-keep-on-relaxed-import-v57`**, **`2026-06-06-future-neg-buy-evening-export-v64`**. 13. **v58 — fixní tarif PV vs. nabíjení (BA81/KV1):** `fixed_horizon_min_sell`; při **`sell > min + 0,20`** + PV → **`bc_pv = 0`**, export FVE; profitable noc **`sell > buy`** mimo `evening_early`. Tag **`2026-06-01-fixed-pv-export-min-sell-charge-v58`**. @@ -141,11 +141,16 @@ flowchart TD 15. **v61 — spot: grid→bat jen při buy ≤ acq:** `sell < buy` ve slotu **není** kritérium (marže); zákaz nabíjení při **`buy > charge_acquisition + degrad`**. Zrušeno v60. Tag **`2026-06-01-spot-grid-charge-at-acq-buy-v61`**. 16. **v63 — Infeasible journal + granulární prep relax (Branch 1):** - - Retry řetězec: strict → `relaxed_expensive_import` → `relaxed_neg_buy_charge` → **`relaxed_neg_prep_hold_only`** (jen prep hold / prep_soc shortfall) → **`relaxed_neg_prep_window`** (navíc vypne neg-evening bundle + tvrdý push) → `neg_sell_phases_fallback`. - - Snap: `relax_chain`, `relaxed_neg_prep_hold_only`; `evening_push_hard_suppressed` jen od `relaxed_neg_prep_window`. + - Retry řetězec: strict → `relaxed_expensive_import` → `relaxed_neg_buy_charge` → **`relaxed_neg_prep_hold_only`** (jen prep hold / prep_soc shortfall) → **`relaxed_neg_prep_window`** (vypne strict pre-neg PV export bundle) → `neg_sell_phases_fallback`. + - Snap: `relax_chain`, `relaxed_neg_prep_hold_only`. - Selhání po celém řetězci → `planning_run.status = failed`, sloupec `error_text`, `ems.fn_planning_run_fail` (aktivní plán se nemění). - Diagnostika: `scripts/diagnose_home01_infeasible.py --print-export-sql --run-id `. Tag **`2026-06-06-infeasible-journal-granular-prep-relax-v63`**. +17. **v64 — future neg-buy večerní export (Branch 2, home-01):** + - **`future_neg_buy_discharge`**: před **`buy<0`** dnem s dostatečnou FVE v **`sell<0`** zůstává neg-evening push + kotvy **`reserve_soc`** i při **`relaxed_neg_prep_window`**. + - **`pos_sell_pre_neg_buy_ge_exempt_slots`**: večerní peak před **`buy<0`** — výjimka z `ge=0` při ekonomicky výhodném vývozu. + - **`terminal_soc_factor_effective`**: × **0,1** při **`future_neg_buy_discharge`**. Snap: `future_neg_buy_discharge`, `evening_push_hard_suppressed` (jen fallback). Tag **`2026-06-06-future-neg-buy-evening-export-v64`**. + **Funkce:** … home-01 **v61**; BA81/KV1 fixed **v59** (+ `R__063`). ### Rozpočet nabíjecích slotů (plánováno, 2026-06) diff --git a/docs/planning-changelog.md b/docs/planning-changelog.md index 8a2968d..9856e15 100644 --- a/docs/planning-changelog.md +++ b/docs/planning-changelog.md @@ -5,6 +5,24 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen --- +## 2026-06-06 — Future neg-buy večerní export (v64, Branch 2) + +**Problém:** home-01 run 23784 při **`relaxed_neg_prep_window`**: `evening_push_hard_suppressed`, prázdné **`neg_evening_push_slots`**, **`pos_sell_pre_neg_buy_ts`** blokoval `ge_bat` ve večerní špičce, terminal SoC shadow price držel ~80 % SoC + import @ ~5 Kč. + +**Změna (v64):** +- **`future_neg_buy_discharge`**: před dnem s **`buy<0`**, pokud FVE v **`sell<0`** pokryje deficit do prep rampy, zůstává neg-evening bundle (push + kotvy **`reserve_soc`**) i při **`relaxed_neg_prep_window`** (strict pre-neg PV export bundle se vypne). +- **`evening_push_hard_suppressed`** jen při **`neg_sell_phases_fallback`**, ne při **`relaxed_neg_prep_window`**. +- **`pos_sell_pre_neg_buy_ge_exempt_slots`**: večerní peak před **`buy<0`** nesmí dostat `ge=0`, pokud je vývoz ekonomicky výhodný. +- **`terminal_soc_factor_effective`**: při **`future_neg_buy_discharge`** násobit **`planner_terminal_soc_value_factor`** × **0,1**. + +**Soubory:** `backend/services/planning_engine.py`, `backend/tests/test_planning_dispatch_milp.py`. + +**Ověření:** +- `pytest backend/tests/test_planning_dispatch_milp.py -k "future_neg_buy or relaxed_neg_prep"` +- MCP: `solver_params->'inputs'->>'future_neg_buy_discharge' = true`, `evening_push_hard_suppressed = false` (bez fallback), večer `grid_setpoint_w < 0` k ~**`reserve_soc`**. + +--- + ## 2026-06-06 — Infeasible journal + granulární prep relax (v63, Branch 1) **Problém:** home-01 run 23784 prošel až **`relaxed_neg_prep_window`** (3. retry) → `evening_push_hard_suppressed`, prázdné `neg_evening_push_slots`, SoC ~80 % ve špičce + import @ ~5 Kč. Selhání **`Solver: Infeasible`** se neukládalo do DB (jen log backendu).