fakt me to nebavi furt jsou tam chyby
This commit is contained in:
@@ -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 17–22h — 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,
|
||||
|
||||
@@ -3698,8 +3698,8 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertLess(results[0].grid_setpoint_w, -500)
|
||||
self.assertLess(results[0].battery_soc_target, 70.0)
|
||||
|
||||
def test_degraded_relaxed_solver_no_night_dump_and_self_consume(self) -> None:
|
||||
"""relaxed_solver_masks: žádný večerní dump za ~2,5 Kč; noc dům z baterie, ne import ~4 Kč."""
|
||||
def test_degraded_relaxed_solver_evening_to_reserve_and_night_self_consume(self) -> None:
|
||||
"""relaxed_solver_masks: večer vývoz k ~20 %, noc dům z baterie (ne import ~4 Kč)."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 6, 6, 21, 30, tzinfo=prague).astimezone(timezone.utc)
|
||||
rows: list[tuple[float, float, int]] = [
|
||||
@@ -3723,7 +3723,10 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
buy, sell, load = rows[i]
|
||||
pv_a, pv_b = 0, 0
|
||||
elif local.hour >= 5 and local.hour < 12:
|
||||
buy, sell, load, pv_a, pv_b = 0.5, -0.3, 800, 2000, 2500
|
||||
if 8 <= local.hour < 11:
|
||||
buy, sell, load, pv_a, pv_b = -0.4, -0.3, 800, 2000, 2500
|
||||
else:
|
||||
buy, sell, load, pv_a, pv_b = 0.5, -0.3, 800, 2000, 2500
|
||||
else:
|
||||
buy, sell, load, pv_a, pv_b = 3.0, 2.0, 500, 0, 0
|
||||
slots.append(
|
||||
@@ -3742,6 +3745,8 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
)
|
||||
bat = _battery(uc_wh=64_000.0, arb_pct=20.0, terminal_soc_value_factor=0.9)
|
||||
bat.planner_neg_sell_prep_soc_percent = 100.0
|
||||
bat.max_charge_power_w = 18_000
|
||||
bat.max_discharge_power_w = 18_000
|
||||
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,
|
||||
@@ -3753,7 +3758,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
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),
|
||||
]
|
||||
soc = 0.71 * bat.soc_max_wh
|
||||
soc = 0.56 * bat.soc_max_wh
|
||||
results, _ms, snap = solve_dispatch(
|
||||
slots,
|
||||
bat,
|
||||
@@ -3774,13 +3779,21 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
)
|
||||
inp = snap.get("inputs") or {}
|
||||
self.assertTrue(inp.get("relaxed_solver_masks"))
|
||||
self.assertGreater(len(inp.get("degraded_relaxed_night_ts") or []), 0)
|
||||
for i, r in enumerate(results[:12]):
|
||||
self.assertGreaterEqual(r.battery_soc_target, 10.0)
|
||||
if i < 4:
|
||||
self.assertGreater(r.grid_setpoint_w, -500, msg=f"slot {i} evening dump")
|
||||
if 8 <= i <= 11 and rows[i][0] > 3.0:
|
||||
self.assertLessEqual(r.grid_setpoint_w, rows[i][2] + 50, msg=f"slot {i} grid import")
|
||||
self.assertGreater(len(inp.get("degraded_evening_export_ts") or []), 0)
|
||||
evening_soc_end = min(r.battery_soc_target for r in results[:8])
|
||||
self.assertLess(evening_soc_end, 55.0)
|
||||
for i in range(8, 12):
|
||||
if rows[i][0] > 3.0:
|
||||
self.assertLessEqual(
|
||||
results[i].grid_setpoint_w,
|
||||
50,
|
||||
msg=f"slot {i} should not import for baseload",
|
||||
)
|
||||
self.assertLess(
|
||||
results[i].battery_setpoint_w,
|
||||
-100,
|
||||
msg=f"slot {i} should discharge for house",
|
||||
)
|
||||
|
||||
def test_kv1_evening_push_profitable_vs_morning_zone_peak(self) -> None:
|
||||
"""v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne."""
|
||||
|
||||
@@ -17,13 +17,14 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
- Nový retry **`relaxed_pos_sell_ge_block`** (+ **`relaxed_solver_masks`** nouzový) v `SOLVER_RELAX_STEPS`.
|
||||
- **v2:** two-pass pass2 dědí všechny relax flagy; při pass2 Infeasible fallback na pass1; override push zrušen už od `relaxed_neg_prep_hold_only`.
|
||||
- **v3 (`degraded-night-guard-v3`):** v **`relaxed_solver_masks`** — spot **bez nočního/večerního `ge_bat` exportu**; **`_degraded_relaxed_night_self_consume_indices`** + tvrdý expensive-import guard (dům z baterie až **`min_soc`**, ne import za ~4 Kč); exportní podlaha SoC ≥ **`reserve_soc`**.
|
||||
- **v4 (`degraded-night-guard-v4`):** oprava v3 — `charge_slots=all` obcházela expensive-import guard → import za ~4 Kč. **Večer D0 (17–22h):** vývoz k **`reserve_soc`** (`degraded_evening_export_ts` + shortfall). **Po 22h / půlnoc:** tvrdý **`bd ≥ load`** (`force_night_self_consume`) i když `t ∈ charge_slots`. Večerní export sloty **ne** sahají do 23h+ (jinak blokují noc).
|
||||
|
||||
**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, testy `test_home01_late_replan_high_soc_realistic_masks`, `test_degraded_relaxed_solver_no_night_dump_and_self_consume`.
|
||||
**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, testy `test_home01_late_replan_high_soc_realistic_masks`, `test_degraded_relaxed_solver_evening_to_reserve_and_night_self_consume`.
|
||||
|
||||
**Ověření:**
|
||||
- `PYTHONPATH=backend python3 scripts/repro_home01_23840.py` → `OK two_pass`
|
||||
- `pytest backend/tests/test_planning_dispatch_milp.py -k "home01_late_replan or degraded_relaxed"`
|
||||
- Po deployi: aktivní run `planner_build_tag` končí **`degraded-night-guard-v3`**; při `relax_chain` obsahujícím `relaxed_solver_masks` večer **bez** masivního exportu, noc **bez** importu pro baseload nad ~load setpoint.
|
||||
- Po deployi: aktivní run `planner_build_tag` končí **`degraded-night-guard-v4`**; při `relax_chain` obsahujícím `relaxed_solver_masks`: večer vývoz k ~**20 %**, noc **bez** importu pro baseload, ráno headroom na FVE + **`buy<0`**.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user