prej final final v2 verze
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-v4"
|
||||
PLANNER_BUILD_TAG = "2026-06-06-home01-strict-late-replan-v5"
|
||||
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"strict",
|
||||
"relaxed_expensive_import",
|
||||
@@ -1940,6 +1940,56 @@ def _evening_push_segment_candidates(
|
||||
return out
|
||||
|
||||
|
||||
def _strict_late_replan_evening_slot_indices(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
first_neg_buy_idx: int | None,
|
||||
observed_soc_wh: float,
|
||||
reserve_soc_wh: float,
|
||||
) -> set[int]:
|
||||
"""
|
||||
Strict solve: večer D0 (17–22h) před dnem s buy<0 — vývoz k reserve, výjimka z pos_sell ge=0.
|
||||
"""
|
||||
if first_neg_buy_idx is None or first_neg_buy_idx <= 0:
|
||||
return set()
|
||||
if not any(float(s.buy_price) < 0.0 for s in slots):
|
||||
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 _strict_late_replan_night_self_consume_indices(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
evening_export_ts: set[int],
|
||||
) -> set[int]:
|
||||
"""Strict: noc po 22h — dům z baterie, ne drahý import (mimo večerní export sloty)."""
|
||||
out: set[int] = set()
|
||||
for t, s in enumerate(slots):
|
||||
if t in evening_export_ts:
|
||||
continue
|
||||
if not _in_night_battery_export_window(s):
|
||||
continue
|
||||
if float(s.load_baseline_w) <= 0:
|
||||
continue
|
||||
if float(s.buy_price) < 0.0:
|
||||
continue
|
||||
out.add(t)
|
||||
return out
|
||||
|
||||
|
||||
def _degraded_relaxed_night_self_consume_indices(
|
||||
slots: list[PlanningSlot],
|
||||
) -> set[int]:
|
||||
@@ -2658,6 +2708,9 @@ def solve_dispatch(
|
||||
or relaxed_solver_masks
|
||||
)
|
||||
prep_hold_relaxed = relaxed_neg_prep_hold_only or relaxed_neg_prep_window
|
||||
late_replan_strict_active = False
|
||||
strict_late_replan_evening_ts: set[int] = set()
|
||||
strict_late_replan_night_ts: set[int] = set()
|
||||
EV = len(vehicles) # počet EV (typicky 2)
|
||||
planner_version_resolved = _planner_engine_version(planner_version)
|
||||
planner_v2 = planner_version_resolved == "v2"
|
||||
@@ -2902,10 +2955,60 @@ def solve_dispatch(
|
||||
)
|
||||
min_spread_pre = float(degradation_cost_effective)
|
||||
fixed_tariff_like_pre = purchase_fixed_pre or _horizon_fixed_tariff_like(slots)
|
||||
reserve_soc_wh_solver = float(
|
||||
getattr(battery, "reserve_soc_wh", getattr(battery, "min_soc_wh", 0.0))
|
||||
)
|
||||
if om == "AUTO" and not purchase_fixed_pre and not relaxed_solver_masks:
|
||||
strict_late_replan_evening_ts = _strict_late_replan_evening_slot_indices(
|
||||
slots,
|
||||
first_neg_buy_idx=first_neg_buy_idx,
|
||||
observed_soc_wh=observed_soc_wh,
|
||||
reserve_soc_wh=reserve_soc_wh_solver,
|
||||
)
|
||||
late_replan_strict_active = bool(strict_late_replan_evening_ts)
|
||||
if late_replan_strict_active:
|
||||
slots = [
|
||||
replace(
|
||||
s,
|
||||
allow_charge=True,
|
||||
allow_discharge_export=True,
|
||||
)
|
||||
if i in strict_late_replan_evening_ts
|
||||
else s
|
||||
for i, s in enumerate(slots)
|
||||
]
|
||||
charge_slots |= strict_late_replan_evening_ts
|
||||
discharge_export_slots |= strict_late_replan_evening_ts
|
||||
if not relaxed_pos_sell_ge_block and not relaxed_solver_masks:
|
||||
slots = _relax_solver_slot_masks(slots)
|
||||
charge_slots = {t for t, s in enumerate(slots) if s.allow_charge}
|
||||
charge_slots |= {
|
||||
t for t, s in enumerate(slots) if float(s.buy_price) < 0.0
|
||||
}
|
||||
charge_slots |= {
|
||||
t
|
||||
for t, s in enumerate(slots)
|
||||
if float(s.sell_price) < 0.0
|
||||
and max(
|
||||
0,
|
||||
int(s.pv_a_forecast_w)
|
||||
+ int(s.pv_b_forecast_w)
|
||||
- int(s.load_baseline_w),
|
||||
)
|
||||
> 500
|
||||
}
|
||||
discharge_export_slots = {
|
||||
t for t, s in enumerate(slots) if s.allow_discharge_export
|
||||
}
|
||||
late_replan_solver_relax = (
|
||||
late_replan_strict_active
|
||||
and not relaxed_solver_masks
|
||||
)
|
||||
neg_sell_phases_en = (
|
||||
om == "AUTO"
|
||||
and not purchase_fixed_pre
|
||||
and _neg_sell_phases_enabled(battery)
|
||||
and not late_replan_strict_active
|
||||
)
|
||||
neg_sell_phase_by_t: list[str] = ["none"] * T
|
||||
neg_sell_soc_target_by_t: list[Optional[float]] = [None] * T
|
||||
@@ -2963,6 +3066,7 @@ def solve_dispatch(
|
||||
and not purchase_fixed_pre
|
||||
and neg_sell_phases_en
|
||||
and not relaxed_neg_prep_window
|
||||
and not late_replan_strict_active
|
||||
)
|
||||
neg_evening_discharge_active = neg_evening_bundle_strict or future_neg_buy_discharge_en
|
||||
if neg_evening_bundle_strict:
|
||||
@@ -3151,7 +3255,9 @@ def solve_dispatch(
|
||||
purchase_fixed=purchase_fixed_pre,
|
||||
)
|
||||
# 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)
|
||||
evening_push_hard_suppressed = bool(
|
||||
neg_sell_phases_fallback or late_replan_strict_active
|
||||
)
|
||||
else:
|
||||
evening_push_hard_suppressed = False
|
||||
last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy(
|
||||
@@ -3169,6 +3275,8 @@ def solve_dispatch(
|
||||
fixed_tariff=fixed_tariff_like_pre,
|
||||
future_neg_buy_discharge_en=future_neg_buy_discharge_en,
|
||||
)
|
||||
if strict_late_replan_evening_ts:
|
||||
pos_sell_pre_neg_buy_ge_exempt_ts |= strict_late_replan_evening_ts
|
||||
pre_neg_buy_empty_ts = _pre_neg_buy_empty_discharge_indices(
|
||||
slots, first_neg_buy_idx, last_pos_sell_pre_neg_buy
|
||||
)
|
||||
@@ -3199,6 +3307,13 @@ def solve_dispatch(
|
||||
slots, evening_push_ts
|
||||
)
|
||||
night_self_consume_discourage_ts |= post_evening_push_night_ts
|
||||
if not relaxed_solver_masks and strict_late_replan_evening_ts:
|
||||
strict_late_replan_night_ts = _strict_late_replan_night_self_consume_indices(
|
||||
slots,
|
||||
evening_export_ts=strict_late_replan_evening_ts,
|
||||
)
|
||||
night_self_consume_discourage_ts |= strict_late_replan_night_ts
|
||||
post_evening_push_night_ts |= strict_late_replan_night_ts
|
||||
battery_export_defer_pv_ts = {
|
||||
t for t in range(T) if _battery_export_push_defer_to_pv(slots[t])
|
||||
}
|
||||
@@ -3343,6 +3458,7 @@ def solve_dispatch(
|
||||
relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
or late_replan_solver_relax
|
||||
):
|
||||
commitment_for_solve = None
|
||||
if commitment_for_solve is not None and len(commitment_for_solve) == T:
|
||||
@@ -3422,7 +3538,7 @@ def solve_dispatch(
|
||||
deg_cap,
|
||||
)
|
||||
degraded_evening_export_shortfall.append((t_deg, sf_deg, deg_cap))
|
||||
if not relaxed_neg_buy_charge:
|
||||
if not (relaxed_neg_buy_charge or late_replan_solver_relax):
|
||||
neg_buy_slot_indices = [
|
||||
t for t, s in enumerate(slots) if float(s.buy_price) < 0.0
|
||||
]
|
||||
@@ -3533,7 +3649,7 @@ def solve_dispatch(
|
||||
cap_w = float(min(pv_surplus_w, battery.max_charge_power_w))
|
||||
sf_pv = pulp.LpVariable(f"post_neg_pv_shortfall_{t}", 0, cap_w)
|
||||
pv_charge_shortfall.append((t, sf_pv, cap_w))
|
||||
if neg_sell_phases_en and not prep_hold_relaxed:
|
||||
if neg_sell_phases_en and not (prep_hold_relaxed or late_replan_strict_active):
|
||||
for t_ns in range(T):
|
||||
phase_ns = neg_sell_phase_by_t[t_ns]
|
||||
tgt_ns = neg_sell_soc_target_by_t[t_ns]
|
||||
@@ -3552,7 +3668,7 @@ def solve_dispatch(
|
||||
continue
|
||||
tail_last_by_day[_prague_calendar_date(st_ln)] = t_ln
|
||||
for t_tail_last in tail_last_by_day.values():
|
||||
if t_tail_last in charge_slots or relaxed_neg_buy_charge:
|
||||
if t_tail_last in charge_slots or relaxed_neg_buy_charge or late_replan_solver_relax:
|
||||
us_tail = pulp.LpVariable(
|
||||
f"neg_sell_tail_soc_{t_tail_last}",
|
||||
0,
|
||||
@@ -3561,7 +3677,7 @@ def solve_dispatch(
|
||||
neg_sell_soc_underfill.append(
|
||||
(t_tail_last, us_tail, float(battery.soc_max_wh))
|
||||
)
|
||||
if not prep_hold_relaxed:
|
||||
if not (prep_hold_relaxed or late_replan_strict_active):
|
||||
for t_ph in range(T):
|
||||
if neg_sell_phase_by_t[t_ph] != "prep":
|
||||
continue
|
||||
@@ -3577,7 +3693,7 @@ def solve_dispatch(
|
||||
prep_hold_curtail_shortfall.append((t_ph, sf_ca, cap_ca))
|
||||
elif len(neg_buy_slot_indices_pre) >= 2:
|
||||
t_nb_last = max(neg_buy_slot_indices_pre)
|
||||
if t_nb_last in charge_slots or relaxed_neg_buy_charge:
|
||||
if t_nb_last in charge_slots or relaxed_neg_buy_charge or late_replan_solver_relax:
|
||||
us = pulp.LpVariable(
|
||||
f"neg_buy_soc_under_{t_nb_last}",
|
||||
0,
|
||||
@@ -4584,13 +4700,13 @@ def solve_dispatch(
|
||||
or force_night_self_consume
|
||||
)
|
||||
)
|
||||
if relaxed_expensive_import and not night_self_consume_slot:
|
||||
if (relaxed_expensive_import or late_replan_solver_relax) 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)
|
||||
or (not (relaxed_expensive_import or late_replan_solver_relax) or night_self_consume_slot)
|
||||
) and om == "AUTO":
|
||||
prob += (
|
||||
bd[t] + pv_ld[t]
|
||||
@@ -5272,6 +5388,16 @@ def solve_dispatch(
|
||||
slots[i].interval_start.isoformat()
|
||||
for i in sorted(degraded_evening_export_ts)
|
||||
],
|
||||
"strict_late_replan_evening_ts": [
|
||||
slots[i].interval_start.isoformat()
|
||||
for i in sorted(strict_late_replan_evening_ts)
|
||||
],
|
||||
"strict_late_replan_night_ts": [
|
||||
slots[i].interval_start.isoformat()
|
||||
for i in sorted(strict_late_replan_night_ts)
|
||||
],
|
||||
"late_replan_strict_active": bool(late_replan_strict_active),
|
||||
"late_replan_solver_relax": bool(late_replan_solver_relax),
|
||||
},
|
||||
"masks": masks_snap,
|
||||
"soc_bounds": soc_bounds_snap,
|
||||
|
||||
@@ -3695,6 +3695,11 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap["inputs"].get("relax_chain"), ["strict"])
|
||||
self.assertNotIn(
|
||||
"relaxed_solver_masks",
|
||||
snap["inputs"].get("relax_chain") or [],
|
||||
)
|
||||
self.assertLess(results[0].grid_setpoint_w, -500)
|
||||
self.assertLess(results[0].battery_soc_target, 70.0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user