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_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-home01-late-replan-infeasible-v1"
|
PLANNER_BUILD_TAG = "2026-06-06-home01-late-replan-infeasible-v2"
|
||||||
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||||
"strict",
|
"strict",
|
||||||
"relaxed_expensive_import",
|
"relaxed_expensive_import",
|
||||||
@@ -80,6 +80,7 @@ SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
|||||||
"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",
|
||||||
)
|
)
|
||||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||||
@@ -151,6 +152,7 @@ def _solver_relax_chain(
|
|||||||
relaxed_neg_prep_window: bool = False,
|
relaxed_neg_prep_window: bool = False,
|
||||||
neg_sell_phases_fallback: bool = False,
|
neg_sell_phases_fallback: bool = False,
|
||||||
relaxed_pos_sell_ge_block: bool = False,
|
relaxed_pos_sell_ge_block: bool = False,
|
||||||
|
relaxed_solver_masks: bool = False,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
flags = {
|
flags = {
|
||||||
"relaxed_expensive_import": relaxed_expensive_import,
|
"relaxed_expensive_import": relaxed_expensive_import,
|
||||||
@@ -159,6 +161,7 @@ def _solver_relax_chain(
|
|||||||
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||||
|
"relaxed_solver_masks": relaxed_solver_masks,
|
||||||
}
|
}
|
||||||
chain = [SOLVER_RELAX_STEPS[0]]
|
chain = [SOLVER_RELAX_STEPS[0]]
|
||||||
for step in SOLVER_RELAX_STEPS[1:]:
|
for step in SOLVER_RELAX_STEPS[1:]:
|
||||||
@@ -2356,8 +2359,52 @@ def _pv_forced_vent_export_allowed(
|
|||||||
return False
|
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]:
|
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")
|
inp = snap.get("inputs")
|
||||||
if not isinstance(inp, dict):
|
if not isinstance(inp, dict):
|
||||||
return {}
|
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_hold_only",
|
||||||
"relaxed_neg_prep_window",
|
"relaxed_neg_prep_window",
|
||||||
"neg_sell_phases_fallback",
|
"neg_sell_phases_fallback",
|
||||||
|
"relaxed_pos_sell_ge_block",
|
||||||
|
"relaxed_solver_masks",
|
||||||
):
|
):
|
||||||
if inp.get(key):
|
if inp.get(key):
|
||||||
out[key] = True
|
out[key] = True
|
||||||
@@ -2443,8 +2492,9 @@ def solve_dispatch_two_pass(
|
|||||||
evening_push_ts_override=None,
|
evening_push_ts_override=None,
|
||||||
**relax_carry,
|
**relax_carry,
|
||||||
)
|
)
|
||||||
except RuntimeError as exc:
|
except (RuntimeError, PlannerSolverError) as exc:
|
||||||
if "Infeasible" in str(exc):
|
infeasible = isinstance(exc, PlannerSolverError) or "Infeasible" in str(exc)
|
||||||
|
if infeasible:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"two_pass pass2 Infeasible (%s), using pass1 solution",
|
"two_pass pass2 Infeasible (%s), using pass1 solution",
|
||||||
exc,
|
exc,
|
||||||
@@ -2471,6 +2521,8 @@ def _evening_push_override_for_solve(
|
|||||||
relaxed_neg_prep_hold_only: bool,
|
relaxed_neg_prep_hold_only: bool,
|
||||||
relaxed_neg_prep_window: bool,
|
relaxed_neg_prep_window: bool,
|
||||||
neg_sell_phases_fallback: bool,
|
neg_sell_phases_fallback: bool,
|
||||||
|
relaxed_pos_sell_ge_block: bool = False,
|
||||||
|
relaxed_solver_masks: bool = False,
|
||||||
) -> Optional[set[int]]:
|
) -> Optional[set[int]]:
|
||||||
"""Po Infeasible nesmí retry držet hysterézní push z minulého běhu."""
|
"""Po Infeasible nesmí retry držet hysterézní push z minulého běhu."""
|
||||||
if evening_push_ts_override is None:
|
if evening_push_ts_override is None:
|
||||||
@@ -2478,8 +2530,11 @@ def _evening_push_override_for_solve(
|
|||||||
if (
|
if (
|
||||||
relaxed_expensive_import
|
relaxed_expensive_import
|
||||||
or relaxed_neg_buy_charge
|
or relaxed_neg_buy_charge
|
||||||
|
or relaxed_neg_prep_hold_only
|
||||||
or relaxed_neg_prep_window
|
or relaxed_neg_prep_window
|
||||||
or neg_sell_phases_fallback
|
or neg_sell_phases_fallback
|
||||||
|
or relaxed_pos_sell_ge_block
|
||||||
|
or relaxed_solver_masks
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
return set(evening_push_ts_override)
|
return set(evening_push_ts_override)
|
||||||
@@ -2529,6 +2584,7 @@ def solve_dispatch(
|
|||||||
relaxed_neg_prep_window: bool = False,
|
relaxed_neg_prep_window: bool = False,
|
||||||
neg_sell_phases_fallback: bool = False,
|
neg_sell_phases_fallback: bool = False,
|
||||||
relaxed_pos_sell_ge_block: bool = False,
|
relaxed_pos_sell_ge_block: bool = False,
|
||||||
|
relaxed_solver_masks: bool = False,
|
||||||
evening_push_ts_override: Optional[set[int]] = None,
|
evening_push_ts_override: Optional[set[int]] = None,
|
||||||
) -> tuple[list[DispatchResult], int, dict[str, Any]]:
|
) -> 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_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 — vypne strict pre-neg bundle; future_neg_buy večerní export 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)
|
T = len(slots)
|
||||||
if T < 1:
|
if T < 1:
|
||||||
raise RuntimeError("solve_dispatch requires at least one slot")
|
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 = (
|
any_relaxed = (
|
||||||
relaxed_expensive_import
|
relaxed_expensive_import
|
||||||
or relaxed_neg_buy_charge
|
or relaxed_neg_buy_charge
|
||||||
or relaxed_neg_prep_window
|
or relaxed_neg_prep_window
|
||||||
or neg_sell_phases_fallback
|
or neg_sell_phases_fallback
|
||||||
or relaxed_pos_sell_ge_block
|
or relaxed_pos_sell_ge_block
|
||||||
|
or relaxed_solver_masks
|
||||||
)
|
)
|
||||||
prep_hold_relaxed = relaxed_neg_prep_hold_only or relaxed_neg_prep_window
|
prep_hold_relaxed = relaxed_neg_prep_hold_only or relaxed_neg_prep_window
|
||||||
EV = len(vehicles) # počet EV (typicky 2)
|
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_hold_only=relaxed_neg_prep_hold_only,
|
||||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
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
|
push_override_eff = None
|
||||||
if push_override_raw:
|
if push_override_raw:
|
||||||
@@ -3113,7 +3175,7 @@ def solve_dispatch(
|
|||||||
for t in discharge_export_slots:
|
for t in discharge_export_slots:
|
||||||
if _prague_calendar_date(slots[t]) == replan_day:
|
if _prague_calendar_date(slots[t]) == replan_day:
|
||||||
charge_slots.add(t)
|
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.
|
# Poslední retry: SQL allow_charge / drahý import nesmí zablokovat fyzicky dosažitelný plán.
|
||||||
charge_slots = set(range(T))
|
charge_slots = set(range(T))
|
||||||
discharge_export_slots = {
|
discharge_export_slots = {
|
||||||
@@ -3123,6 +3185,16 @@ def solve_dispatch(
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
battery_export_defer_pv_ts = set()
|
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(
|
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
|
||||||
slots,
|
slots,
|
||||||
first_neg_buy_idx=first_neg_buy_idx,
|
first_neg_buy_idx=first_neg_buy_idx,
|
||||||
@@ -4620,6 +4692,39 @@ def solve_dispatch(
|
|||||||
relaxed_pos_sell_ge_block=True,
|
relaxed_pos_sell_ge_block=True,
|
||||||
evening_push_ts_override=evening_push_ts_override,
|
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(
|
raise PlannerSolverError(
|
||||||
pulp.LpStatus[status],
|
pulp.LpStatus[status],
|
||||||
relax_chain=_solver_relax_chain(
|
relax_chain=_solver_relax_chain(
|
||||||
@@ -4629,6 +4734,7 @@ def solve_dispatch(
|
|||||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
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,
|
"relaxed_neg_prep_window": relaxed_neg_prep_window,
|
||||||
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
"neg_sell_phases_fallback": neg_sell_phases_fallback,
|
||||||
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
"relaxed_pos_sell_ge_block": relaxed_pos_sell_ge_block,
|
||||||
|
"relaxed_solver_masks": relaxed_solver_masks,
|
||||||
"relax_chain": _solver_relax_chain(
|
"relax_chain": _solver_relax_chain(
|
||||||
relaxed_expensive_import=relaxed_expensive_import,
|
relaxed_expensive_import=relaxed_expensive_import,
|
||||||
relaxed_neg_buy_charge=relaxed_neg_buy_charge,
|
relaxed_neg_buy_charge=relaxed_neg_buy_charge,
|
||||||
@@ -4988,6 +5095,7 @@ def solve_dispatch(
|
|||||||
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
relaxed_neg_prep_window=relaxed_neg_prep_window,
|
||||||
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
neg_sell_phases_fallback=neg_sell_phases_fallback,
|
||||||
relaxed_pos_sell_ge_block=relaxed_pos_sell_ge_block,
|
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_buy_czk_kwh": charge_acquisition_czk_kwh,
|
||||||
"charge_acquisition_cutoff_at": (
|
"charge_acquisition_cutoff_at": (
|
||||||
@@ -5092,6 +5200,13 @@ async def run_daily_plan(
|
|||||||
)
|
)
|
||||||
planner_version_resolved = _planner_engine_version(planner_version)
|
planner_version_resolved = _planner_engine_version(planner_version)
|
||||||
slots = await _load_slots(site_id, horizon_from, horizon_to, db, soc_wh=soc_wh)
|
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"
|
om = operating_mode or "AUTO"
|
||||||
try:
|
try:
|
||||||
@@ -5282,6 +5397,13 @@ async def run_rolling_replan(
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
slots = await _load_slots(site_id, replan_from, horizon_to, db, soc_wh=soc_wh)
|
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
|
# 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í).
|
# ems.fn_load_planning_slots_full. Pro audit/debug ale chceme ukládat i RAW (bez korekcí).
|
||||||
correction_factor, correction_log = 1.0, {
|
correction_factor, correction_log = 1.0, {
|
||||||
|
|||||||
@@ -3285,7 +3285,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
||||||
|
|
||||||
def test_evening_push_override_cleared_on_relaxed_retry(self) -> None:
|
def test_evening_push_override_cleared_on_relaxed_retry(self) -> None:
|
||||||
"""v53: hysterézní override se nepřenáší do Infeasible retry větví."""
|
"""v53/v2: hysterézní override se nepřenáší do Infeasible retry větví."""
|
||||||
kept = _evening_push_override_for_solve(
|
kept = _evening_push_override_for_solve(
|
||||||
{2, 5},
|
{2, 5},
|
||||||
relaxed_expensive_import=False,
|
relaxed_expensive_import=False,
|
||||||
@@ -3295,7 +3295,8 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
neg_sell_phases_fallback=False,
|
neg_sell_phases_fallback=False,
|
||||||
)
|
)
|
||||||
self.assertEqual(kept, {2, 5})
|
self.assertEqual(kept, {2, 5})
|
||||||
kept_prep_hold = _evening_push_override_for_solve(
|
# v2: stale override from active plan must drop already at prep_hold_only
|
||||||
|
dropped_prep_hold = _evening_push_override_for_solve(
|
||||||
{2, 5},
|
{2, 5},
|
||||||
relaxed_expensive_import=False,
|
relaxed_expensive_import=False,
|
||||||
relaxed_neg_buy_charge=False,
|
relaxed_neg_buy_charge=False,
|
||||||
@@ -3303,7 +3304,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
relaxed_neg_prep_window=False,
|
relaxed_neg_prep_window=False,
|
||||||
neg_sell_phases_fallback=False,
|
neg_sell_phases_fallback=False,
|
||||||
)
|
)
|
||||||
self.assertEqual(kept_prep_hold, {2, 5})
|
self.assertIsNone(dropped_prep_hold)
|
||||||
dropped = _evening_push_override_for_solve(
|
dropped = _evening_push_override_for_solve(
|
||||||
{2, 5},
|
{2, 5},
|
||||||
relaxed_expensive_import=True,
|
relaxed_expensive_import=True,
|
||||||
|
|||||||
@@ -11,16 +11,18 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
|||||||
|
|
||||||
**Příčina:** SQL maska `allow_charge=false` ve večerních slotech (drahý `buy`, `sell` < `buy`) + guard drahého importu vyžadoval baseload z baterie (`bd`), zatímco **v64 `future_neg_buy_discharge`** současně vynucoval večerní vývoz — LP bez rozšíření `charge_slots` neměl řešení.
|
**Příčina:** SQL maska `allow_charge=false` ve večerních slotech (drahý `buy`, `sell` < `buy`) + guard drahého importu vyžadoval baseload z baterie (`bd`), zatímco **v64 `future_neg_buy_discharge`** současně vynucoval večerní vývoz — LP bez rozšíření `charge_slots` neměl řešení.
|
||||||
|
|
||||||
**Oprava (tag `2026-06-06-home01-late-replan-infeasible-v1`):**
|
**Oprava (tag `2026-06-06-home01-late-replan-infeasible-v1`, doplněno **v2**):**
|
||||||
- Při **`future_neg_buy_discharge`**: rozšířit `charge_slots` o večerní / exportní sloty dne replanu (grid smí krmit load během vývozu).
|
- Při **`future_neg_buy_discharge`**: rozšířit `charge_slots` o večerní / exportní sloty dne replanu (grid smí krmit load během vývozu).
|
||||||
- Nový poslední retry **`relaxed_pos_sell_ge_block`** (+ nouzové rozšíření masek) v `SOLVER_RELAX_STEPS`.
|
- **`_unlock_late_replan_evening_slots`** po `fn_load_planning_slots_full` — večer D0 `allow_charge` + export z DB.
|
||||||
|
- 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`.
|
||||||
|
|
||||||
**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, test `test_home01_late_replan_high_soc_realistic_masks`.
|
**Soubory:** `planning_engine.py`, `scripts/repro_home01_23840.py`, test `test_home01_late_replan_high_soc_realistic_masks`.
|
||||||
|
|
||||||
**Ověření:**
|
**Ověření:**
|
||||||
- `PYTHONPATH=backend python3 scripts/repro_home01_23840.py` → `OK two_pass`
|
- `PYTHONPATH=backend python3 scripts/repro_home01_23840.py` → `OK two_pass`
|
||||||
- `pytest backend/tests/test_planning_dispatch_milp.py -k home01_late_replan`
|
- `pytest backend/tests/test_planning_dispatch_milp.py -k home01_late_replan`
|
||||||
- Po deployi: ruční replan v AUTO → `planning_run.status=active`, večerní sloty `grid_setpoint_w < 0`.
|
- Po deployi: ruční replan v AUTO → `planning_run.status=active`, `planner_build_tag` končí **`infeasible-v2`**, večerní sloty `grid_setpoint_w < 0`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user