Branch 2: home-01 neg-večer — export k reserve_soc, fix pos_sell_pre_neg_buy + oddělit evening_push od prep relax
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_CHARGE_PENALTY_CZK_KWH = 400.0
|
||||||
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
||||||
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.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, ...] = (
|
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||||
"strict",
|
"strict",
|
||||||
"relaxed_expensive_import",
|
"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.
|
# 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_MAX_SLACK_WH = 400.0
|
||||||
NEG_EVENING_RESERVE_SOC_SLACK_PENALTY_CZK_PER_WH = 55.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.
|
# 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_FORECAST_MARGIN = 1.15
|
||||||
PRE_NEG_PV_EXPORT_MIN_NEEDED_WH = 2500.0
|
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(
|
def _neg_evening_before_neg_push_indices(
|
||||||
slots: list[PlanningSlot],
|
slots: list[PlanningSlot],
|
||||||
candidate_ts: set[int],
|
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_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_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_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)
|
T = len(slots)
|
||||||
if T < 1:
|
if T < 1:
|
||||||
@@ -2577,11 +2673,7 @@ def solve_dispatch(
|
|||||||
if horizon_slots_h24 > 0
|
if horizon_slots_h24 > 0
|
||||||
else 4.0
|
else 4.0
|
||||||
)
|
)
|
||||||
terminal_factor = float(battery.planner_terminal_soc_value_factor)
|
terminal_factor_base = 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
|
|
||||||
)
|
|
||||||
|
|
||||||
charge_acq_raw = getattr(slots[0], "charge_acquisition_buy_czk_kwh", None)
|
charge_acq_raw = getattr(slots[0], "charge_acquisition_buy_czk_kwh", None)
|
||||||
charge_acquisition_czk_kwh = (
|
charge_acquisition_czk_kwh = (
|
||||||
@@ -2676,12 +2768,38 @@ def solve_dispatch(
|
|||||||
neg_evening_push_ts: set[int] = set()
|
neg_evening_push_ts: set[int] = set()
|
||||||
neg_evening_export_budget_wh: float | None = None
|
neg_evening_export_budget_wh: float | None = None
|
||||||
neg_evening_reserve_anchors: list[tuple[int, float]] = []
|
neg_evening_reserve_anchors: list[tuple[int, float]] = []
|
||||||
|
future_neg_buy_discharge_en = False
|
||||||
if (
|
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"
|
om == "AUTO"
|
||||||
and not purchase_fixed_pre
|
and not purchase_fixed_pre
|
||||||
and neg_sell_phases_en
|
and neg_sell_phases_en
|
||||||
and not relaxed_neg_prep_window
|
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(
|
pre_neg_pv_export_ts, pre_neg_cushion_by_day = _pre_neg_pv_export_bundle(
|
||||||
slots,
|
slots,
|
||||||
battery,
|
battery,
|
||||||
@@ -2690,9 +2808,13 @@ def solve_dispatch(
|
|||||||
neg_sell_phases_en=True,
|
neg_sell_phases_en=True,
|
||||||
soc_target_by_t=neg_sell_soc_target_by_t,
|
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(
|
neg_evening_before_neg_ts = _evening_discharge_before_neg_day_ts(
|
||||||
slots,
|
slots,
|
||||||
neg_sell_day_meta,
|
meta_for_evening,
|
||||||
)
|
)
|
||||||
neg_evening_before_neg_ts |= _discharge_before_first_neg_sell_ts(
|
neg_evening_before_neg_ts |= _discharge_before_first_neg_sell_ts(
|
||||||
slots,
|
slots,
|
||||||
@@ -2700,7 +2822,7 @@ def solve_dispatch(
|
|||||||
)
|
)
|
||||||
neg_evening_reserve_anchors = _neg_evening_reserve_soc_anchors(
|
neg_evening_reserve_anchors = _neg_evening_reserve_soc_anchors(
|
||||||
slots,
|
slots,
|
||||||
neg_sell_day_meta,
|
meta_for_evening,
|
||||||
battery,
|
battery,
|
||||||
)
|
)
|
||||||
reserve_wh = float(
|
reserve_wh = float(
|
||||||
@@ -2856,10 +2978,8 @@ def solve_dispatch(
|
|||||||
first_neg_sell_idx=first_neg_sell_idx,
|
first_neg_sell_idx=first_neg_sell_idx,
|
||||||
kv1_evening_push=kv1_evening_push_pre,
|
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).
|
# Tvrdý ge_bat push vypnout jen při neg_sell fallback (ne při prep relax — v64).
|
||||||
evening_push_hard_suppressed = bool(
|
evening_push_hard_suppressed = bool(neg_sell_phases_fallback)
|
||||||
relaxed_neg_prep_window or neg_sell_phases_fallback
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
evening_push_hard_suppressed = False
|
evening_push_hard_suppressed = False
|
||||||
last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy(
|
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(
|
pos_sell_pre_neg_buy_ts = _positive_sell_pre_neg_buy_indices(
|
||||||
slots, first_neg_buy_idx
|
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(
|
pre_neg_buy_empty_ts = _pre_neg_buy_empty_discharge_indices(
|
||||||
slots, first_neg_buy_idx, last_pos_sell_pre_neg_buy
|
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
|
first_neg_buy_idx is not None
|
||||||
and first_neg_buy_idx > 0
|
and first_neg_buy_idx > 0
|
||||||
and t in pos_sell_pre_neg_buy_ts
|
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[t] == 0
|
||||||
prob += ge_pv[t] == 0
|
prob += ge_pv[t] == 0
|
||||||
@@ -4332,7 +4462,7 @@ def solve_dispatch(
|
|||||||
if not relaxed_neg_prep_window:
|
if not relaxed_neg_prep_window:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"solve_dispatch still Infeasible, retry with relaxed_neg_prep_window "
|
"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(
|
return solve_dispatch(
|
||||||
slots,
|
slots,
|
||||||
@@ -4754,6 +4884,12 @@ def solve_dispatch(
|
|||||||
push_override_raw and not push_override_eff
|
push_override_raw and not push_override_eff
|
||||||
),
|
),
|
||||||
"evening_push_hard_suppressed": bool(evening_push_hard_suppressed),
|
"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(
|
"evening_push_peak_fallback_used": bool(
|
||||||
om == "AUTO"
|
om == "AUTO"
|
||||||
and not computed_evening_push_ts
|
and not computed_evening_push_ts
|
||||||
|
|||||||
@@ -3326,8 +3326,8 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
self.assertFalse(snap["inputs"].get("evening_push_hard_suppressed"))
|
self.assertFalse(snap["inputs"].get("evening_push_hard_suppressed"))
|
||||||
self.assertLess(results[0].grid_setpoint_w, -1000)
|
self.assertLess(results[0].grid_setpoint_w, -1000)
|
||||||
|
|
||||||
def test_relaxed_neg_prep_suppresses_hard_push_only(self) -> None:
|
def test_relaxed_neg_prep_keeps_hard_push_v64(self) -> None:
|
||||||
"""v57: relaxed_neg_prep_window vypne jen tvrdý push, ne seznam slotů."""
|
"""v64: relaxed_neg_prep_window nesmí vypnout tvrdý evening push (jen fallback)."""
|
||||||
prague = ZoneInfo("Europe/Prague")
|
prague = ZoneInfo("Europe/Prague")
|
||||||
slots = [
|
slots = [
|
||||||
PlanningSlot(
|
PlanningSlot(
|
||||||
@@ -3377,7 +3377,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
current_tuv_temp_c=55.0,
|
current_tuv_temp_c=55.0,
|
||||||
relaxed_neg_prep_window=True,
|
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 []
|
push_iso = snap["inputs"].get("evening_push_ts") or []
|
||||||
self.assertGreaterEqual(len(push_iso), 1)
|
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:
|
def test_kv1_evening_push_profitable_vs_morning_zone_peak(self) -> None:
|
||||||
"""v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne."""
|
"""v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne."""
|
||||||
prague = ZoneInfo("Europe/Prague")
|
prague = ZoneInfo("Europe/Prague")
|
||||||
|
|||||||
@@ -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`**.
|
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`**.
|
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`**.
|
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):**
|
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`.
|
- 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`; `evening_push_hard_suppressed` jen od `relaxed_neg_prep_window`.
|
- 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í).
|
- 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 <id>`. Tag **`2026-06-06-infeasible-journal-granular-prep-relax-v63`**.
|
- Diagnostika: `scripts/diagnose_home01_infeasible.py --print-export-sql --run-id <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`).
|
**Funkce:** … home-01 **v61**; BA81/KV1 fixed **v59** (+ `R__063`).
|
||||||
|
|
||||||
### Rozpočet nabíjecích slotů (plánováno, 2026-06)
|
### Rozpočet nabíjecích slotů (plánováno, 2026-06)
|
||||||
|
|||||||
@@ -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)
|
## 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).
|
**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).
|
||||||
|
|||||||
Reference in New Issue
Block a user