oprava
Some checks failed
CI and deploy / migration-check (push) Failing after 36s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-29 22:26:52 +02:00
parent 88df09640c
commit 230351b38a
2 changed files with 116 additions and 17 deletions

View File

@@ -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-29-neg-prep-observed-soc-v40"
PLANNER_BUILD_TAG = "2026-05-29-neg-prep-infeasible-relax-v40b"
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
NEG_SELL_POST_DETACH_BCPV_DISCOURAGE_CZK_KWH = 250.0
# Večer před neg dnem: výboj do sítě (měkký shortfall na ge_bat).
@@ -1300,12 +1300,20 @@ def _neg_evening_before_neg_push_indices(
*,
export_budget_wh: float,
per_slot_discharge_wh: float,
discharge_export_ok: set[int] | None = None,
) -> set[int]:
"""Nejdražší kladné-sell sloty v kandidátech, dokud budget z pozorovaného SoC."""
if export_budget_wh < per_slot_discharge_wh * 0.5 or not candidate_ts:
return set()
eligible = {
t
for t in candidate_ts
if discharge_export_ok is None or t in discharge_export_ok
}
if not eligible:
return set()
ranked = sorted(
candidate_ts,
eligible,
key=lambda t: (float(slots[t].sell_price), -t),
reverse=True,
)
@@ -1974,6 +1982,8 @@ def solve_dispatch(
planner_version: str | None = None,
relaxed_expensive_import: bool = False,
relaxed_neg_buy_charge: bool = False,
relaxed_neg_prep_window: bool = False,
neg_sell_phases_fallback: bool = False,
evening_push_ts_override: Optional[set[int]] = None,
) -> tuple[list[DispatchResult], int, dict[str, Any]]:
"""
@@ -1981,6 +1991,7 @@ def solve_dispatch(
Vrátí (výsledky, solver_duration_ms, solver_debug_snapshot).
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_window: třetí retry — bez tvrdého večerního push/kotvy a prep hold binárek (sell<0 okno).
"""
T = len(slots)
if T < 1:
@@ -2256,7 +2267,12 @@ 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]] = []
if om == "AUTO" and not purchase_fixed_pre and neg_sell_phases_en:
if (
om == "AUTO"
and not purchase_fixed_pre
and neg_sell_phases_en
and not relaxed_neg_prep_window
):
pre_neg_pv_export_ts, pre_neg_cushion_by_day = _pre_neg_pv_export_bundle(
slots,
battery,
@@ -2298,6 +2314,7 @@ def solve_dispatch(
neg_evening_before_neg_ts,
export_budget_wh=float(neg_evening_export_budget_wh),
per_slot_discharge_wh=per_slot_neg_eve_wh,
discharge_export_ok=discharge_export_slots,
)
elif om == "AUTO" and not purchase_fixed_pre:
legacy_ok = bool(
@@ -2678,6 +2695,7 @@ def solve_dispatch(
neg_sell_soc_underfill.append(
(t_tail_last, us_tail, float(battery.soc_max_wh))
)
if not relaxed_neg_prep_window:
for t_ph in range(T):
if neg_sell_phase_by_t[t_ph] != "prep":
continue
@@ -3643,6 +3661,7 @@ def solve_dispatch(
charge_commitment_prev_w=charge_commitment_prev_w,
planner_version=planner_version,
relaxed_expensive_import=True,
evening_push_ts_override=evening_push_ts_override,
)
if not relaxed_neg_buy_charge:
logger.warning(
@@ -3664,6 +3683,60 @@ def solve_dispatch(
relaxed_expensive_import=True,
relaxed_neg_buy_charge=True,
)
if not relaxed_neg_prep_window:
logger.warning(
"solve_dispatch still Infeasible, retry with relaxed_neg_prep_window "
"(skip evening push/anchors and prep hold hard constraints)"
)
return 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=planner_version,
relaxed_expensive_import=True,
relaxed_neg_buy_charge=True,
relaxed_neg_prep_window=True,
neg_sell_phases_fallback=neg_sell_phases_fallback,
evening_push_ts_override=evening_push_ts_override,
)
if not neg_sell_phases_fallback:
logger.warning(
"solve_dispatch still Infeasible, retry with neg_sell phases disabled "
"(prep_soc_percent=100)"
)
battery_no_phases = SimpleNamespace(
**{
**vars(battery),
"planner_neg_sell_prep_soc_percent": 100.0,
}
)
return solve_dispatch(
slots,
battery_no_phases,
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=planner_version,
relaxed_expensive_import=True,
relaxed_neg_buy_charge=True,
relaxed_neg_prep_window=True,
neg_sell_phases_fallback=True,
evening_push_ts_override=evening_push_ts_override,
)
raise RuntimeError(f"Solver: {pulp.LpStatus[status]}")
# --- Post-processing ---
@@ -3988,6 +4061,9 @@ def solve_dispatch(
),
"load_first_enabled": om == "AUTO",
"relaxed_expensive_import": relaxed_expensive_import,
"relaxed_neg_buy_charge": relaxed_neg_buy_charge,
"relaxed_neg_prep_window": relaxed_neg_prep_window,
"neg_sell_phases_fallback": neg_sell_phases_fallback,
"charge_acquisition_buy_czk_kwh": charge_acquisition_czk_kwh,
"charge_acquisition_cutoff_at": (
slots[0].charge_acquisition_cutoff_at.isoformat()
@@ -4208,11 +4284,24 @@ async def run_rolling_replan(
planner_version=planner_version_resolved,
)
logger.info(f"[site={site_id}] Rolling replan from {replan_from}{horizon_to}")
logger.info(
"[site=%s] Rolling replan from %s%s (tag=%s)",
site_id,
replan_from,
horizon_to,
PLANNER_BUILD_TAG,
)
battery, hp, grid, vehicles, ev_sessions, soc_wh, tuv_temp, operating_mode, tuv_stats = (
await _load_site_context(site_id, db)
)
if operating_mode != "AUTO":
logger.info(
"[site=%s] Rolling replan skipped: operating_mode=%s (not AUTO)",
site_id,
operating_mode,
)
return None, None
slots = await _load_slots(site_id, replan_from, horizon_to, db, soc_wh=soc_wh)
# PV forecast korekce je kanonicky v DB (delta + rolling faktor + decay) a do LP vstupuje přes

View File

@@ -5,6 +5,16 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
---
## 2026-05-29 — Infeasible rolling: relax neg-prep okno (v40b)
**Problém:** Po načtení OTE na **30. 5.** (neg sell) rolling/home-01 končil `Solver: Infeasible` od ~13:15; ruční replan stejně. Plán zůstal na runu z 13:00 (horizont jen do 22:00). Log často prázdný — výjimka se loguje na `WARNING`, scheduler ji polyká.
**Změna (v40b):** Třetí retry `relaxed_neg_prep_window` (bez večerního push/kotvy + prep hold binárek); čtvrtý retry s `planner_neg_sell_prep_soc_percent=100` (fáze sell&lt;0 vypnuté). Večerní push jen sloty s `allow_discharge_export`. Rolling v **MANUAL** se přeskočí (log INFO). Tag **`2026-05-29-neg-prep-infeasible-relax-v40b`**.
**Ověření:** po deployi `POST …/plan/run?type=rolling` v AUTO; `solver_params.inputs.relaxed_neg_prep_window` nebo `neg_sell_phases_fallback`; log: `docker compose -f deploy/docker-compose.yml logs backend --since 2h 2>&1 | rg -i infeasible`.
---
## 2026-05-29 — Neg-prep z pozorovaného SoC (Plan 5, v40)
**Problém:** Strategie „místo na zítřejší FVE + sell&lt;0“ a večerní výboj před neg dnem počítaly z **modelového** SoC (řetězení `soc_target` mezi dny v `_pre_neg_pv_export_bundle`). BMS měl často **~15 %** více → předčasné zastavení výboje, „mrtvé“ kWh přes noc, méně ranního pre-neg exportu.