fix refaktoru
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-23 22:20:25 +02:00
parent e3e5fc138c
commit dbc004a949
3 changed files with 89 additions and 26 deletions

View File

@@ -50,9 +50,9 @@ DEFAULT_PLANNER_DISCHARGE_RELAX_PREWINDOW_SLOTS = 8
# Penalizace je v Kč/Wh (např. 0.20 = 200 Kč/kWh). Musí být dost velká, aby přebila
# bezpečnostní SoC buffer + terminal shadow cenu a solver skutečně „dovylil“ před sell<0.
PRENEG_SELL_SOC_ANCHOR_SLACK_PENALTY_CZK_PER_WH = 0.20
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 12.0
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 40.0
# Měkký tlak: v okně sell<0 + block_export využít PV přebytek do baterie (ne curtail).
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 8.0
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 25.0
CORRECTION_WINDOW_H = 1 # hodina zpět pro výpočet korekčního faktoru
CORRECTION_MIN_CLAMP = 0.5 # spodní limit korekčního faktoru
CORRECTION_MAX_CLAMP = 1.5 # horní limit korekčního faktoru
@@ -643,11 +643,13 @@ def _slot_profitable_battery_export(
min_spread: float,
fixed_tariff: bool,
) -> bool:
"""Export z baterie do sítě má kladnou marži oproti acquisition / fixnímu buy."""
"""
Export z baterie do sítě má kladnou marži vs. cena zásoby (acquisition).
U fixed tarifu nepoužívat buy v slotu (může být predikovaný OTE jiný den) — jen acquisition.
"""
sell_t = float(slot.sell_price)
if fixed_tariff:
return sell_t > float(slot.buy_price) + min_spread
return sell_t > charge_acquisition_czk_kwh + min_spread
acq = float(charge_acquisition_czk_kwh)
return sell_t > acq + min_spread
def _horizon_fixed_tariff_like(slots: list[PlanningSlot]) -> bool:
@@ -1106,6 +1108,19 @@ def solve_dispatch(
else min(float(s.buy_price) for s in slots)
)
min_spread_pre = float(degradation_cost_effective)
fixed_tariff_like_pre = _horizon_fixed_tariff_like(slots)
profitable_export_ts_pre: set[int] = set()
if om == "AUTO":
for _t in range(T):
if _t not in discharge_export_slots:
continue
if _slot_profitable_battery_export(
slots[_t],
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=min_spread_pre,
fixed_tariff=fixed_tariff_like_pre,
):
profitable_export_ts_pre.add(_t)
if first_neg_sell_idx is not None and first_neg_sell_idx > 0 and floor_pct is not None:
# Kotva na ranním peaku (ne na posledním slotu před sell<0) — jinak dump až v 07:30.
if (
@@ -1165,7 +1180,7 @@ def solve_dispatch(
peak_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
pv_charge_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
fixed_tariff_like = _horizon_fixed_tariff_like(slots)
fixed_tariff_like = fixed_tariff_like_pre
block_export_neg_sell = bool(getattr(grid, "block_export_on_negative_sell", False))
if om == "AUTO":
for t in range(T):
@@ -1282,28 +1297,19 @@ def solve_dispatch(
1000.0,
)
if om == "AUTO":
profitable_export_ts: set[int] = set()
for t in range(T):
if t not in discharge_export_slots:
continue
if _slot_profitable_battery_export(
slots[t],
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=min_spread_pre,
fixed_tariff=fixed_tariff_like,
):
profitable_export_ts.add(t)
profitable_export_ts = profitable_export_ts_pre
export_push_w = min(
float(EVENING_BATTERY_EXPORT_MIN_W),
float(battery.max_discharge_power_w),
float(grid.max_export_power_w),
)
for t_peak in morning_pre_neg_export_ts:
if t_peak in profitable_export_ts:
prob += ge_bat[t_peak] >= PRENEG_MORNING_EXPORT_MIN_W * z_export[t_peak]
prob += ge_bat[t_peak] >= float(PRENEG_MORNING_EXPORT_MIN_W) * z_export[t_peak]
for t_peak in evening_peak_export_ts:
if t_peak in profitable_export_ts:
prob += ge_bat[t_peak] >= EVENING_BATTERY_EXPORT_MIN_W * z_export[t_peak]
# Všechny ekonomicky výhodné discharge sloty (ne jen „globální maximum“ high_sell).
for t_peak in profitable_export_ts:
if t_peak in morning_pre_neg_export_ts or t_peak in evening_peak_export_ts:
continue
prob += ge_bat[t_peak] >= EVENING_BATTERY_EXPORT_MIN_W * z_export[t_peak]
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
# Ostatní profitable sloty: jen shortfall penalizace (ne tvrdý push na celý horizont).
if t_anchor is not None and soc_anchor_slack is not None:
target_floor_wh = float(planner_floor_effective_wh)
prob += soc[t_anchor] <= target_floor_wh + soc_anchor_slack
@@ -1524,7 +1530,11 @@ def solve_dispatch(
# Safety export floor: v běžných (ne high-sell) slotech nevybít exportem energii potřebnou pro
# robustnost/noční baseload. Použije se pouze pokud je safety target v SQL vyplněný.
tgt_s = slots[t].safety_soc_target_wh if daytime_en else None
if tgt_s is not None and not high_sell_slot[t]:
if (
tgt_s is not None
and not high_sell_slot[t]
and t not in profitable_export_ts_pre
):
export_soc_floor_t = max(
export_soc_floor_t,
min(