aa zas oprava
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-late-replan-infeasible-v1"
|
||||
PLANNER_BUILD_TAG = "2026-06-06-home01-late-replan-infeasible-v2"
|
||||
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"strict",
|
||||
"relaxed_expensive_import",
|
||||
@@ -80,6 +80,7 @@ SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"relaxed_neg_prep_window",
|
||||
"neg_sell_phases_fallback",
|
||||
"relaxed_pos_sell_ge_block",
|
||||
"relaxed_solver_masks",
|
||||
)
|
||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||
@@ -151,6 +152,7 @@ def _solver_relax_chain(
|
||||
relaxed_neg_prep_window: bool = False,
|
||||
neg_sell_phases_fallback: bool = False,
|
||||
relaxed_pos_sell_ge_block: bool = False,
|
||||
relaxed_solver_masks: bool = False,
|
||||
) -> list[str]:
|
||||
flags = {
|
||||
"relaxed_expensive_import": relaxed_expensive_import,
|
||||
@@ -159,6 +161,7 @@ def _solver_relax_chain(
|
||||
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||
"relaxed_solver_masks": relaxed_solver_masks,
|
||||
}
|
||||
chain = [SOLVER_RELAX_STEPS[0]]
|
||||
for step in SOLVER_RELAX_STEPS[1:]:
|
||||
@@ -2356,8 +2359,52 @@ def _pv_forced_vent_export_allowed(
|
||||
return False
|
||||
|
||||
|
||||
def _relax_solver_slot_masks(slots: list[PlanningSlot]) -> list[PlanningSlot]:
|
||||
"""Nouzově permissivní allow_* — SQL masky nesmí učinit LP neřešitelným."""
|
||||
return [
|
||||
replace(
|
||||
s,
|
||||
allow_charge=True,
|
||||
allow_discharge_export=float(s.sell_price) >= 0.0,
|
||||
)
|
||||
for s in slots
|
||||
]
|
||||
|
||||
|
||||
def _unlock_late_replan_evening_slots(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
current_soc_wh: float,
|
||||
reserve_soc_wh: float,
|
||||
) -> None:
|
||||
"""Pozdní replan: večer D0 povolit grid import + export (SQL allow_charge často false)."""
|
||||
if not slots or current_soc_wh <= float(reserve_soc_wh) + 500.0:
|
||||
return
|
||||
if not any(float(s.buy_price) < 0.0 for s in slots):
|
||||
return
|
||||
replan_day = _prague_calendar_date(slots[0])
|
||||
unlocked = 0
|
||||
for i, s in enumerate(slots):
|
||||
if _prague_calendar_date(s) != replan_day:
|
||||
continue
|
||||
if float(s.sell_price) < 0.0:
|
||||
continue
|
||||
if not _in_evening_push_hour_window(s):
|
||||
continue
|
||||
if s.allow_charge and s.allow_discharge_export:
|
||||
continue
|
||||
slots[i] = replace(s, allow_charge=True, allow_discharge_export=True)
|
||||
unlocked += 1
|
||||
if unlocked:
|
||||
logger.info(
|
||||
"Late replan: unlocked evening slot masks on %d slot(s) (soc=%.0f Wh)",
|
||||
unlocked,
|
||||
float(current_soc_wh),
|
||||
)
|
||||
|
||||
|
||||
def _solve_dispatch_relax_carryover(snap: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Pass2 two-pass: neopakovat Infeasible řetězec, pokud pass1 skončil v nouzovém režimu."""
|
||||
"""Pass2 two-pass: přenést nouzové relax flagy z pass1, ať pass2 nezačne od strict."""
|
||||
inp = snap.get("inputs")
|
||||
if not isinstance(inp, dict):
|
||||
return {}
|
||||
@@ -2368,6 +2415,8 @@ def _solve_dispatch_relax_carryover(snap: dict[str, Any]) -> dict[str, Any]:
|
||||
"relaxed_neg_prep_hold_only",
|
||||
"relaxed_neg_prep_window",
|
||||
"neg_sell_phases_fallback",
|
||||
"relaxed_pos_sell_ge_block",
|
||||
"relaxed_solver_masks",
|
||||
):
|
||||
if inp.get(key):
|
||||
out[key] = True
|
||||
@@ -2443,8 +2492,9 @@ def solve_dispatch_two_pass(
|
||||
evening_push_ts_override=None,
|
||||
**relax_carry,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
if "Infeasible" in str(exc):
|
||||
except (RuntimeError, PlannerSolverError) as exc:
|
||||
infeasible = isinstance(exc, PlannerSolverError) or "Infeasible" in str(exc)
|
||||
if infeasible:
|
||||
logger.warning(
|
||||
"two_pass pass2 Infeasible (%s), using pass1 solution",
|
||||
exc,
|
||||
@@ -2471,6 +2521,8 @@ def _evening_push_override_for_solve(
|
||||
relaxed_neg_prep_hold_only: bool,
|
||||
relaxed_neg_prep_window: bool,
|
||||
neg_sell_phases_fallback: bool,
|
||||
relaxed_pos_sell_ge_block: bool = False,
|
||||
relaxed_solver_masks: bool = False,
|
||||
) -> Optional[set[int]]:
|
||||
"""Po Infeasible nesmí retry držet hysterézní push z minulého běhu."""
|
||||
if evening_push_ts_override is None:
|
||||
@@ -2478,8 +2530,11 @@ def _evening_push_override_for_solve(
|
||||
if (
|
||||
relaxed_expensive_import
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_hold_only
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
or relaxed_pos_sell_ge_block
|
||||
or relaxed_solver_masks
|
||||
):
|
||||
return None
|
||||
return set(evening_push_ts_override)
|
||||
@@ -2529,6 +2584,7 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_window: bool = False,
|
||||
neg_sell_phases_fallback: bool = False,
|
||||
relaxed_pos_sell_ge_block: bool = False,
|
||||
relaxed_solver_masks: bool = False,
|
||||
evening_push_ts_override: Optional[set[int]] = None,
|
||||
) -> tuple[list[DispatchResult], int, dict[str, Any]]:
|
||||
"""
|
||||
@@ -2538,17 +2594,21 @@ def solve_dispatch(
|
||||
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_window: čtvrtý retry — vypne strict pre-neg bundle; future_neg_buy večerní export zůstává.
|
||||
relaxed_pos_sell_ge_block: poslední retry — neaplikovat ge=0 v pos_sell před buy<0 (zbylá Infeasible).
|
||||
relaxed_pos_sell_ge_block: retry — neaplikovat ge=0 v pos_sell před buy<0.
|
||||
relaxed_solver_masks: poslední retry — permissivní SQL masky + vypnutí tvrdých neg-večer větví.
|
||||
"""
|
||||
T = len(slots)
|
||||
if T < 1:
|
||||
raise RuntimeError("solve_dispatch requires at least one slot")
|
||||
if relaxed_solver_masks or relaxed_pos_sell_ge_block:
|
||||
slots = _relax_solver_slot_masks(slots)
|
||||
any_relaxed = (
|
||||
relaxed_expensive_import
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
or relaxed_pos_sell_ge_block
|
||||
or relaxed_solver_masks
|
||||
)
|
||||
prep_hold_relaxed = relaxed_neg_prep_hold_only or relaxed_neg_prep_window
|
||||
EV = len(vehicles) # počet EV (typicky 2)
|
||||
@@ -3013,6 +3073,8 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_hold_only=relaxed_neg_prep_hold_only,
|
||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
||||
relaxed_solver_masks=relaxed_solver_masks,
|
||||
)
|
||||
push_override_eff = None
|
||||
if push_override_raw:
|
||||
@@ -3113,7 +3175,7 @@ def solve_dispatch(
|
||||
for t in discharge_export_slots:
|
||||
if _prague_calendar_date(slots[t]) == replan_day:
|
||||
charge_slots.add(t)
|
||||
if relaxed_pos_sell_ge_block:
|
||||
if relaxed_pos_sell_ge_block or relaxed_solver_masks:
|
||||
# Poslední retry: SQL allow_charge / drahý import nesmí zablokovat fyzicky dosažitelný plán.
|
||||
charge_slots = set(range(T))
|
||||
discharge_export_slots = {
|
||||
@@ -3123,6 +3185,16 @@ def solve_dispatch(
|
||||
}
|
||||
else:
|
||||
battery_export_defer_pv_ts = set()
|
||||
if relaxed_solver_masks and om == "AUTO":
|
||||
future_neg_buy_discharge_en = False
|
||||
neg_evening_discharge_active = False
|
||||
neg_evening_push_ts = set()
|
||||
neg_evening_before_neg_ts = set()
|
||||
neg_evening_reserve_anchors = []
|
||||
evening_push_ts = set()
|
||||
evening_early_export_penalty_ts = set()
|
||||
battery_export_defer_pv_ts = set()
|
||||
evening_push_hard_suppressed = True
|
||||
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
|
||||
slots,
|
||||
first_neg_buy_idx=first_neg_buy_idx,
|
||||
@@ -4620,6 +4692,39 @@ def solve_dispatch(
|
||||
relaxed_pos_sell_ge_block=True,
|
||||
evening_push_ts_override=evening_push_ts_override,
|
||||
)
|
||||
if not relaxed_solver_masks:
|
||||
logger.warning(
|
||||
"solve_dispatch still Infeasible, retry with relaxed_solver_masks "
|
||||
"(permissive slot masks; neg-evening hard bundle off)"
|
||||
)
|
||||
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_hold_only=True,
|
||||
relaxed_neg_prep_window=True,
|
||||
neg_sell_phases_fallback=True,
|
||||
relaxed_pos_sell_ge_block=True,
|
||||
relaxed_solver_masks=True,
|
||||
evening_push_ts_override=evening_push_ts_override,
|
||||
)
|
||||
raise PlannerSolverError(
|
||||
pulp.LpStatus[status],
|
||||
relax_chain=_solver_relax_chain(
|
||||
@@ -4629,6 +4734,7 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
||||
relaxed_solver_masks=relaxed_solver_masks,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4981,6 +5087,7 @@ def solve_dispatch(
|
||||
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||
"relaxed_solver_masks": relaxed_solver_masks,
|
||||
"relax_chain": _solver_relax_chain(
|
||||
relaxed_expensive_import=relaxed_expensive_import,
|
||||
relaxed_neg_buy_charge=relaxed_neg_buy_charge,
|
||||
@@ -4988,6 +5095,7 @@ def solve_dispatch(
|
||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
||||
relaxed_solver_masks=relaxed_solver_masks,
|
||||
),
|
||||
"charge_acquisition_buy_czk_kwh": charge_acquisition_czk_kwh,
|
||||
"charge_acquisition_cutoff_at": (
|
||||
@@ -5092,6 +5200,13 @@ async def run_daily_plan(
|
||||
)
|
||||
planner_version_resolved = _planner_engine_version(planner_version)
|
||||
slots = await _load_slots(site_id, horizon_from, horizon_to, db, soc_wh=soc_wh)
|
||||
_unlock_late_replan_evening_slots(
|
||||
slots,
|
||||
current_soc_wh=float(soc_wh),
|
||||
reserve_soc_wh=float(
|
||||
getattr(battery, "reserve_soc_wh", getattr(battery, "arb_floor_wh", 0.0))
|
||||
),
|
||||
)
|
||||
|
||||
om = operating_mode or "AUTO"
|
||||
try:
|
||||
@@ -5282,6 +5397,13 @@ async def run_rolling_replan(
|
||||
return None, None
|
||||
|
||||
slots = await _load_slots(site_id, replan_from, horizon_to, db, soc_wh=soc_wh)
|
||||
_unlock_late_replan_evening_slots(
|
||||
slots,
|
||||
current_soc_wh=float(soc_wh),
|
||||
reserve_soc_wh=float(
|
||||
getattr(battery, "reserve_soc_wh", getattr(battery, "arb_floor_wh", 0.0))
|
||||
),
|
||||
)
|
||||
# PV forecast korekce je kanonicky v DB (delta + rolling faktor + decay) a do LP vstupuje přes
|
||||
# ems.fn_load_planning_slots_full. Pro audit/debug ale chceme ukládat i RAW (bez korekcí).
|
||||
correction_factor, correction_log = 1.0, {
|
||||
|
||||
Reference in New Issue
Block a user