fakt me to nebavi furt jsou tam chyby
Some checks failed
CI and deploy / migration-check (push) Failing after 12s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-06-06 23:58:01 +02:00
parent b7903db714
commit 50ac40868d
3 changed files with 127 additions and 32 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-06-06-home01-degraded-night-guard-v3"
PLANNER_BUILD_TAG = "2026-06-06-home01-degraded-night-guard-v4"
SOLVER_RELAX_STEPS: tuple[str, ...] = (
"strict",
"relaxed_expensive_import",
@@ -1958,6 +1958,35 @@ def _degraded_relaxed_night_self_consume_indices(
return out
def _degraded_relaxed_evening_export_to_reserve_indices(
slots: list[PlanningSlot],
*,
observed_soc_wh: float,
reserve_soc_wh: float,
first_neg_buy_idx: int | None,
) -> set[int]:
"""
Nouzový solve: večer D0 smí vývoz bat k reserve_soc před dnem s buy<0 (headroom na zítra).
Jen kalendářní večer 1722h — po 22h už noc (dům z baterie, ne držet kvůli exportu).
"""
if first_neg_buy_idx is None or first_neg_buy_idx <= 0:
return set()
if observed_soc_wh <= float(reserve_soc_wh) + 500.0:
return set()
replan_day = _prague_calendar_date(slots[0])
out: set[int] = set()
for t, s in enumerate(slots):
if _prague_calendar_date(s) != replan_day:
continue
h = _prague_hour(s)
if h < NIGHT_EXPORT_EVENING_START_HOUR or h > 22:
continue
if float(s.sell_price) < 0.0:
continue
out.add(t)
return out
def _post_evening_push_night_self_consume_indices(
slots: list[PlanningSlot],
evening_push_ts: set[int],
@@ -3046,6 +3075,7 @@ def solve_dispatch(
night_self_consume_discourage_ts: set[int] = set()
post_evening_push_night_ts: set[int] = set()
degraded_relaxed_night_ts: set[int] = set()
degraded_evening_export_ts: set[int] = set()
evening_push_hysteresis_retained = False
push_override_raw: Optional[set[int]] = None
push_override_eff: Optional[set[int]] = None
@@ -3215,6 +3245,15 @@ def solve_dispatch(
battery_export_defer_pv_ts = set()
evening_push_hard_suppressed = True
degraded_relaxed_night_ts = _degraded_relaxed_night_self_consume_indices(slots)
reserve_wh_degraded = float(
getattr(battery, "reserve_soc_wh", getattr(battery, "min_soc_wh", 0.0))
)
degraded_evening_export_ts = _degraded_relaxed_evening_export_to_reserve_indices(
slots,
observed_soc_wh=observed_soc_wh,
reserve_soc_wh=reserve_wh_degraded,
first_neg_buy_idx=first_neg_buy_idx,
)
night_self_consume_discourage_ts |= degraded_relaxed_night_ts
post_evening_push_night_ts |= degraded_relaxed_night_ts
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
@@ -3325,6 +3364,7 @@ def solve_dispatch(
neg_buy_charge_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
pre_neg_batt_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
pre_neg_buy_empty_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
degraded_evening_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
fixed_tariff_like = fixed_tariff_like_pre
block_export_neg_sell = bool(getattr(grid, "block_export_on_negative_sell", False))
if om == "AUTO":
@@ -3373,6 +3413,15 @@ def solve_dispatch(
continue
sf_e = pulp.LpVariable(f"pre_neg_buy_empty_sf_{t_empty}", 0, export_cap_w)
pre_neg_buy_empty_shortfall.append((t_empty, sf_e, export_cap_w))
if relaxed_solver_masks and degraded_evening_export_ts:
deg_cap = _battery_export_cap_w(battery, grid)
for t_deg in sorted(degraded_evening_export_ts):
sf_deg = pulp.LpVariable(
f"deg_eve_reserve_export_{t_deg}",
0,
deg_cap,
)
degraded_evening_export_shortfall.append((t_deg, sf_deg, deg_cap))
if not relaxed_neg_buy_charge:
neg_buy_slot_indices = [
t for t, s in enumerate(slots) if float(s.buy_price) < 0.0
@@ -3681,6 +3730,10 @@ def solve_dispatch(
sf * PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH * INTERVAL_H / 1000.0
for _t, sf, _cap in peak_export_shortfall
)
+ pulp.lpSum(
sf * NEG_EVENING_PREP_DISCHARGE_SHORTFALL_PENALTY_CZK_KWH * INTERVAL_H / 1000.0
for _t, sf, _cap in degraded_evening_export_shortfall
)
+ pulp.lpSum(
sf * PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH * INTERVAL_H / 1000.0
for _t, sf, _cap in pv_charge_shortfall
@@ -3817,6 +3870,8 @@ def solve_dispatch(
prob += sf >= cap_w - ge_pv[t_sf]
for t_sf, sf, cap_w in neg_evening_before_neg_shortfall:
prob += sf >= cap_w - ge_bat[t_sf]
for t_sf, sf, cap_w in degraded_evening_export_shortfall:
prob += sf >= cap_w - ge_bat[t_sf]
for t_sl, sl, reserve_tgt in neg_evening_reserve_soc_slack:
prob += soc[t_sl] <= float(reserve_tgt) + sl
preneg_export_min_soc_wh = float(min_soc_wh) + max(
@@ -3878,13 +3933,23 @@ def solve_dispatch(
continue
prob += ge_bat[t_pv] == 0
prob += z_export[t_pv] == 0
# Nouzový relax: spot v noci neexportovat baterii za ~2,5 Kč (žádný tvrdý evening dump).
# Nouzový relax: v noci jen vývoz k reserve večer D0; jinak ge_bat=0.
if relaxed_solver_masks and not purchase_fixed_pre:
reserve_wh_blk = float(
getattr(battery, "reserve_soc_wh", getattr(battery, "min_soc_wh", 0.0))
)
for t_blk in range(T):
if t_blk in degraded_evening_export_ts:
continue
if not _in_night_battery_export_window(slots[t_blk]):
continue
prob += ge_bat[t_blk] == 0
prob += z_export[t_blk] == 0
for t_ev in sorted(degraded_evening_export_ts):
m_soc_deg = float(battery.usable_capacity_wh)
prob += soc[t_ev] >= float(reserve_wh_blk) - m_soc_deg * (
1 - z_export[t_ev]
)
# Ostatní profitable sloty: měkká shortfall penalizace (ne večerní push).
if (
last_pos_sell_pre_neg_buy is not None
@@ -4500,25 +4565,37 @@ def solve_dispatch(
expensive_import_slot = expensive_import_slot or (
buy_t > charge_acquisition_czk_kwh + min_spread
)
if expensive_import_slot and t not in charge_slots and buy_t >= 0.0:
# Strict: síť jen EV+TČ; baseload z baterie/FVE.
# Relaxed: síť smí baseload jen mimo night_self_consume (v46).
night_self_consume_slot = (
om == "AUTO"
and (
t in night_self_consume_discourage_ts
or t in post_evening_push_night_ts
)
if expensive_import_slot and buy_t >= 0.0:
force_night_self_consume = (
relaxed_solver_masks
and t in degraded_relaxed_night_ts
and t not in degraded_evening_export_ts
)
if relaxed_expensive_import and not night_self_consume_slot:
prob += gi[t] <= ev_cap_t + hp[t] + float(s.load_baseline_w)
else:
prob += gi[t] <= ev_cap_t + hp[t]
if (not relaxed_expensive_import or night_self_consume_slot) and om == "AUTO":
prob += (
bd[t] + pv_ld[t]
>= float(s.load_baseline_w) + hp[t]
if force_night_self_consume or (
expensive_import_slot and t not in charge_slots
):
# Strict: síť jen EV+TČ; baseload z baterie/FVE.
# Relaxed: síť smí baseload jen mimo night_self_consume (v46).
night_self_consume_slot = (
om == "AUTO"
and (
t in night_self_consume_discourage_ts
or t in post_evening_push_night_ts
or force_night_self_consume
)
)
if relaxed_expensive_import and not night_self_consume_slot:
prob += gi[t] <= ev_cap_t + hp[t] + float(s.load_baseline_w)
else:
prob += gi[t] <= ev_cap_t + hp[t]
if (
force_night_self_consume
or (not relaxed_expensive_import or night_self_consume_slot)
) and om == "AUTO":
prob += (
bd[t] + pv_ld[t]
>= float(s.load_baseline_w) + hp[t]
)
# Anti souběžný vývoz FVE + významný import (mikrocyklus).
if buy_t > sell_t + min_spread and pv_surplus_w > 0:
prob += ge_pv[t] <= pv_surplus_w
@@ -5191,6 +5268,10 @@ def solve_dispatch(
slots[i].interval_start.isoformat()
for i in sorted(degraded_relaxed_night_ts)
],
"degraded_evening_export_ts": [
slots[i].interval_start.isoformat()
for i in sorted(degraded_evening_export_ts)
],
},
"masks": masks_snap,
"soc_bounds": soc_bounds_snap,