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

This commit is contained in:
Dusan Vojacek
2026-05-25 01:27:33 +02:00
parent 095676e3b1
commit 254508fe1a
4 changed files with 90 additions and 26 deletions

View File

@@ -64,7 +64,7 @@ NEG_SELL_PV_B_VENT_PENALTY_CZK_KWH = 4.0
# Výboj baterie při sell<0 jen těsně před extrémně záporným buy (round-trip arbitráž).
EXTREME_BUY_DUMP_PREWINDOW_SLOTS = 12
NEG_SELL_BAT_DUMP_SHORTFALL_PENALTY_CZK_KWH = 80.0
PLANNER_BUILD_TAG = "2026-05-27-simple-buy-neg-window-v16"
PLANNER_BUILD_TAG = "2026-05-27-site-export-cap-from-db-v18"
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
@@ -792,8 +792,14 @@ def _prague_calendar_date(slot: PlanningSlot):
MORNING_PRENEG_START_HOUR = 5
MORNING_PRENEG_END_HOUR = 11
PRENEG_MORNING_EXPORT_MIN_W = 8_000.0
EVENING_BATTERY_EXPORT_MIN_W = 8_000.0
def _battery_export_cap_w(battery: Any, grid: Any) -> float:
"""Max výkon vývozu baterie do sítě [W] — z DB, ne hardcoded konstanta."""
return min(
float(battery.max_discharge_power_w),
float(grid.max_export_power_w),
)
def _prague_hour(slot: PlanningSlot) -> int:
@@ -1470,11 +1476,7 @@ def solve_dispatch(
)
neg_sell_soc_underfill.append((t, us))
for t in neg_sell_bat_dump_slots:
dump_target_w = min(
float(EVENING_BATTERY_EXPORT_MIN_W),
float(battery.max_discharge_power_w),
float(grid.max_export_power_w),
)
dump_target_w = _battery_export_cap_w(battery, grid)
sf_dump = pulp.LpVariable(f"neg_bat_dump_shortfall_{t}", 0, dump_target_w)
neg_sell_bat_dump_shortfall.append((t, sf_dump, dump_target_w))
@@ -1610,24 +1612,21 @@ def solve_dispatch(
)
if om == "AUTO":
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),
)
export_push_w = _battery_export_cap_w(battery, grid)
for t_peak in morning_pre_neg_export_ts:
if t_peak in profitable_export_ts:
prob += ge_bat[t_peak] >= float(PRENEG_MORNING_EXPORT_MIN_W) * z_export[t_peak]
evening_export_push_w = export_push_w
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
evening_push_ts = _evening_battery_export_push_indices(
slots,
profitable_export_ts=profitable_export_ts,
degrad_czk_kwh=float(degradation_cost_effective),
)
for t_peak in evening_push_ts:
if t_peak not in discharge_export_slots:
continue
prob += ge_bat[t_peak] >= evening_export_push_w * z_export[t_peak]
# Push jen při reálném večerním okně (≥2 sloty); 1-slot regresní testy bez tvrdého push.
if len(evening_push_ts) >= 2:
for t_peak in evening_push_ts:
if t_peak not in discharge_export_slots:
continue
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)
@@ -1947,19 +1946,39 @@ def solve_dispatch(
)
if om == "AUTO":
for t in range(T):
s = slots[t]
sell_t_pre = float(s.sell_price)
pv_surplus_for_gi = max(
0,
int(s.pv_a_forecast_w)
+ int(s.pv_b_forecast_w)
- int(s.load_baseline_w),
)
# Grid→bat (bc_gi): R__063 dává allow_charge=true ze dvou různých důvodů:
# (a) ekonomicky výhodný grid charge slot (nízký buy, výhodná arbitráž),
# (b) sell<0 + pv_surplus (= "povolit PV nabíjení aby pole A nešlo do mínusu").
# V druhém případě bc_gi NESMÍ být povoleno (home-01 run 16652: 09:1509:45
# nabíjelo 18 kW ze sítě za buy 1,11,2 Kč jen proto, že sell=0,2).
# Druhý případ poznáme přes `sell<0 + pv_surplus>0`.
if (
t in charge_slots
and sell_t_pre < 0
and pv_surplus_for_gi > 0
and float(s.buy_price) >= 0.0
):
prob += bc_gi[t] == 0
if t not in charge_slots:
s = slots[t]
pv_surplus_w = max(
0,
int(s.pv_a_forecast_w)
+ int(s.pv_b_forecast_w)
- int(s.load_baseline_w),
)
if float(s.buy_price) >= 0.0:
prob += bc_gi[t] == 0
in_pre_neg_buy_window = (
_neg_buy_idx_main is not None and t < _neg_buy_idx_main
)
if float(s.buy_price) >= 0.0:
prob += bc_gi[t] == 0
if pv_surplus_w <= 0:
prob += bc_pv[t] == 0
elif in_pre_neg_buy_window:
@@ -2044,6 +2063,18 @@ def solve_dispatch(
)
):
prob += ge_pv[t] == 0
# Při `sell < 0` exportovat MAX pole B (má green bonus 7+ Kč/kWh → čistá hodnota
# i při sell=-1 = +6 Kč). Pole A green bonus nemá → export A za sell<0 je čistá ztráta.
# Constraint: ge_pv ≤ pv_b_forecast_w (pole A jde do baterie / curtail).
# Aplikuje se jen u sites bez block_export_on_negative_sell (home-01 áno; KV1 ne)
# A jen pokud reálně existuje pole B (pv_b_forecast_w > 0 — jinak by ge_pv ≤ 0
# zablokovalo legitimní pre-neg-pv export pole A z testů).
if (
sell_t < 0
and float(s.pv_b_forecast_w) > 0
and not getattr(grid, "block_export_on_negative_sell", False)
):
prob += ge_pv[t] <= float(s.pv_b_forecast_w)
# Drahý nákup: dům + TČ z baterie (ne import ze sítě); síť jen EV (+ případně TČ).
# Spot (home-01): buy > min ne-záporného buy v horizontu.
# Fixní tarif (KV1): navíc buy > charge_acquisition (konstantní buy ≈ ref).