oprave vercerniho nevyprodeje
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-05-31-evening-push-any-relaxed-v55"
|
||||
PLANNER_BUILD_TAG = "2026-06-01-evening-push-keep-on-relaxed-import-v57"
|
||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
|
||||
@@ -1922,6 +1922,39 @@ def _evening_battery_export_push_indices(
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def _evening_push_peak_fallback_indices(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
charge_acquisition_czk_kwh: float,
|
||||
min_spread: float,
|
||||
discharge_export_ok: set[int] | None,
|
||||
first_neg_sell_idx: int | None,
|
||||
kv1_evening_push: bool,
|
||||
) -> set[int]:
|
||||
"""Alespoň jeden večerní peak slot (sell desc), když rozpočet Wh nevybral žádný push."""
|
||||
best_t: int | None = None
|
||||
best_sell = -1.0
|
||||
for t, s in enumerate(slots):
|
||||
if discharge_export_ok is not None and t not in discharge_export_ok:
|
||||
continue
|
||||
if not _in_evening_push_hour_window(s):
|
||||
continue
|
||||
if not _slot_evening_push_profitable(
|
||||
s,
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
min_spread=min_spread,
|
||||
slots=slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push,
|
||||
):
|
||||
continue
|
||||
sell_t = float(s.sell_price)
|
||||
if sell_t > best_sell:
|
||||
best_sell = sell_t
|
||||
best_t = t
|
||||
return {best_t} if best_t is not None else set()
|
||||
|
||||
|
||||
def _evening_night_peak_sell_czk(slots: list[PlanningSlot]) -> float:
|
||||
sells = [
|
||||
float(s.sell_price)
|
||||
@@ -2190,22 +2223,33 @@ def solve_dispatch_two_pass(
|
||||
|
||||
slots2 = _slots_with_charge_acquisition(slots, acq2)
|
||||
relax_carry = _solve_dispatch_relax_carryover(snap1)
|
||||
results2, ms2, snap2 = solve_dispatch(
|
||||
slots2,
|
||||
battery,
|
||||
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,
|
||||
evening_push_ts_override=None,
|
||||
**relax_carry,
|
||||
)
|
||||
try:
|
||||
results2, ms2, snap2 = solve_dispatch(
|
||||
slots2,
|
||||
battery,
|
||||
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,
|
||||
evening_push_ts_override=None,
|
||||
**relax_carry,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
if "Infeasible" in str(exc):
|
||||
logger.warning(
|
||||
"two_pass pass2 Infeasible (%s), using pass1 solution",
|
||||
exc,
|
||||
)
|
||||
if isinstance(snap1.get("inputs"), dict):
|
||||
snap1["inputs"]["two_pass_pass2_infeasible_used_pass1"] = True
|
||||
return results1, ms1, snap1
|
||||
raise
|
||||
if isinstance(snap2.get("inputs"), dict):
|
||||
snap2["inputs"]["acquisition_pass1_czk_kwh"] = round(acq1, 6)
|
||||
snap2["inputs"]["acquisition_pass2_czk_kwh"] = round(acq2, 6)
|
||||
@@ -2291,6 +2335,12 @@ def solve_dispatch(
|
||||
T = len(slots)
|
||||
if T < 1:
|
||||
raise RuntimeError("solve_dispatch requires at least one slot")
|
||||
any_relaxed = (
|
||||
relaxed_expensive_import
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
)
|
||||
EV = len(vehicles) # počet EV (typicky 2)
|
||||
planner_version_resolved = _planner_engine_version(planner_version)
|
||||
planner_v2 = planner_version_resolved == "v2"
|
||||
@@ -2675,6 +2725,8 @@ def solve_dispatch(
|
||||
evening_push_hysteresis_retained = False
|
||||
push_override_raw: Optional[set[int]] = None
|
||||
push_override_eff: Optional[set[int]] = None
|
||||
computed_evening_push_ts: set[int] = set()
|
||||
evening_push_hard_suppressed = False
|
||||
if om == "AUTO":
|
||||
per_slot_discharge_wh_pre = max(
|
||||
float(battery.max_discharge_power_w)
|
||||
@@ -2730,14 +2782,21 @@ def solve_dispatch(
|
||||
evening_push_hysteresis_retained = True
|
||||
else:
|
||||
evening_push_ts = computed_evening_push_ts
|
||||
if (
|
||||
relaxed_expensive_import
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_neg_prep_window
|
||||
or neg_sell_phases_fallback
|
||||
):
|
||||
evening_push_ts = set()
|
||||
evening_push_hysteresis_retained = False
|
||||
if not evening_push_ts:
|
||||
evening_push_ts = _evening_push_peak_fallback_indices(
|
||||
slots,
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
min_spread=float(degradation_cost_effective),
|
||||
discharge_export_ok=discharge_export_slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
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).
|
||||
evening_push_hard_suppressed = bool(
|
||||
relaxed_neg_prep_window or neg_sell_phases_fallback
|
||||
)
|
||||
else:
|
||||
evening_push_hard_suppressed = False
|
||||
last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy(
|
||||
slots, first_neg_buy_idx
|
||||
)
|
||||
@@ -3382,35 +3441,38 @@ def solve_dispatch(
|
||||
profitable_export_ts = profitable_export_ts_pre
|
||||
export_push_w = _battery_export_cap_w(battery, grid)
|
||||
discharge_floor_wh = _planner_discharge_floor_wh(battery)
|
||||
for t_peak in morning_pre_neg_export_ts:
|
||||
if t_peak in profitable_export_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_peak]):
|
||||
# Tvrdý ranní/pre-neg export jen ve strict režimu (jinak ~25 % SoC + neg den → Infeasible).
|
||||
if not any_relaxed:
|
||||
for t_peak in morning_pre_neg_export_ts:
|
||||
if t_peak in profitable_export_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_peak]):
|
||||
continue
|
||||
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
for t_pnd in pre_neg_buy_discharge_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_pnd]):
|
||||
continue
|
||||
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
for t_pnd in pre_neg_buy_discharge_ts:
|
||||
if _battery_export_push_defer_to_pv(slots[t_pnd]):
|
||||
continue
|
||||
prob += ge_bat[t_pnd] >= export_push_w * z_export[t_pnd]
|
||||
for t_empty in pre_neg_buy_empty_ts:
|
||||
if t_empty in discharge_export_slots:
|
||||
if _battery_export_push_defer_to_pv(slots[t_empty]):
|
||||
continue
|
||||
prob += ge_bat[t_empty] >= export_push_w * z_export[t_empty]
|
||||
prob += ge_bat[t_pnd] >= export_push_w * z_export[t_pnd]
|
||||
for t_empty in pre_neg_buy_empty_ts:
|
||||
if t_empty in discharge_export_slots:
|
||||
if _battery_export_push_defer_to_pv(slots[t_empty]):
|
||||
continue
|
||||
prob += ge_bat[t_empty] >= export_push_w * z_export[t_empty]
|
||||
for t_early in sorted(evening_early_export_penalty_ts):
|
||||
prob += ge_bat[t_early] == 0
|
||||
for t_peak in sorted(evening_push_ts):
|
||||
if t_peak not in discharge_export_slots:
|
||||
continue
|
||||
if t_peak in battery_export_defer_pv_ts:
|
||||
continue
|
||||
push_floor_w = _evening_push_battery_export_w(
|
||||
slots[t_peak], battery, grid
|
||||
)
|
||||
if push_floor_w >= GE_MIN_EXPORT_W:
|
||||
prob += z_export[t_peak] == 1
|
||||
prob += ge_bat[t_peak] >= push_floor_w
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
if not evening_push_hard_suppressed:
|
||||
for t_peak in sorted(evening_push_ts):
|
||||
if t_peak not in discharge_export_slots:
|
||||
continue
|
||||
if t_peak in battery_export_defer_pv_ts:
|
||||
continue
|
||||
push_floor_w = _evening_push_battery_export_w(
|
||||
slots[t_peak], battery, grid
|
||||
)
|
||||
if push_floor_w >= GE_MIN_EXPORT_W:
|
||||
prob += z_export[t_peak] == 1
|
||||
prob += ge_bat[t_peak] >= push_floor_w
|
||||
prob += soc[t_peak] >= float(discharge_floor_wh)
|
||||
for t_pv in sorted(battery_export_defer_pv_ts):
|
||||
if t_pv in evening_push_ts:
|
||||
continue
|
||||
@@ -4525,15 +4587,20 @@ def solve_dispatch(
|
||||
"evening_push_override_filtered_empty": bool(
|
||||
push_override_raw and not push_override_eff
|
||||
),
|
||||
"evening_push_cleared_on_relaxed_prep": bool(
|
||||
relaxed_neg_prep_window
|
||||
or relaxed_neg_buy_charge
|
||||
or relaxed_expensive_import
|
||||
or neg_sell_phases_fallback
|
||||
"evening_push_hard_suppressed": bool(evening_push_hard_suppressed),
|
||||
"evening_push_peak_fallback_used": bool(
|
||||
om == "AUTO"
|
||||
and not computed_evening_push_ts
|
||||
and bool(evening_push_ts)
|
||||
and not push_override_eff
|
||||
),
|
||||
"charge_commitment_ignored_on_relaxed": bool(
|
||||
commitment_for_solve is None and charge_commitment_prev_w is not None
|
||||
),
|
||||
"morning_pre_neg_export_hard": bool(
|
||||
om == "AUTO" and not any_relaxed and bool(morning_pre_neg_export_ts)
|
||||
),
|
||||
"any_relaxed_solve": bool(any_relaxed),
|
||||
"kv1_evening_push_morning_peak_rule": _kv1_block_export_fixed_evening_push(
|
||||
grid,
|
||||
purchase_fixed=purchase_fixed_pre,
|
||||
|
||||
Reference in New Issue
Block a user