dalsi fix zapornoeho sellu u home-01
This commit is contained in:
@@ -59,7 +59,12 @@ NEG_SELL_CURTAIL_PENALTY_CZK_KWH = 1.0
|
||||
NEG_SELL_PV_CHARGE_REWARD_CZK_KWH = 0.8
|
||||
# Měkký tlak: v okně sell<0 dobít na soc_max (ne zastavit na ~94 % kvůli curtail).
|
||||
NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH = 0.35
|
||||
PLANNER_BUILD_TAG = "2026-05-25-home01-neg-sell-evening-v10"
|
||||
# Jen ventil nekontrolovatelného pole B při plné baterii a sell<0 (spot); ne celý PV přebytek.
|
||||
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-26-neg-sell-bat-dump-extreme-buy-v11"
|
||||
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
|
||||
@@ -688,6 +693,70 @@ def _horizon_fixed_tariff_like(slots: list[PlanningSlot]) -> bool:
|
||||
return max(buys) - min(buys) < 0.25
|
||||
|
||||
|
||||
def _future_extreme_buy_from(
|
||||
slots: list[PlanningSlot],
|
||||
buy_thr: float,
|
||||
) -> list[bool]:
|
||||
"""True v t, pokud v některém budoucím slotu buy <= buy_thr."""
|
||||
t_len = len(slots)
|
||||
out = [False] * t_len
|
||||
seen = False
|
||||
for i in range(t_len - 1, -1, -1):
|
||||
if float(slots[i].buy_price) <= buy_thr:
|
||||
seen = True
|
||||
out[i] = seen
|
||||
return out
|
||||
|
||||
|
||||
def _neg_sell_bat_dump_slots(
|
||||
slots: list[PlanningSlot],
|
||||
*,
|
||||
operating_mode: str,
|
||||
purchase_fixed: bool,
|
||||
grid: Any,
|
||||
buy_extreme_thr: float,
|
||||
degrad_czk_kwh: float,
|
||||
) -> set[int]:
|
||||
"""Sloty, kde smí ge_bat>0 při sell<0 (výboj před extrémně záporným buy)."""
|
||||
if operating_mode != "AUTO" or purchase_fixed:
|
||||
return set()
|
||||
if bool(getattr(grid, "block_export_on_negative_sell", False)):
|
||||
return set()
|
||||
t_len = len(slots)
|
||||
future_extreme = _future_extreme_buy_from(slots, buy_extreme_thr)
|
||||
dist = _slots_until_buy_le(slots, buy_extreme_thr)
|
||||
out: set[int] = set()
|
||||
for t, s in enumerate(slots):
|
||||
if float(s.sell_price) >= 0.0:
|
||||
continue
|
||||
future_min = min(
|
||||
(float(slots[j].buy_price) for j in range(t + 1, t_len)),
|
||||
default=float(s.buy_price),
|
||||
)
|
||||
if (
|
||||
future_extreme[t]
|
||||
and 0 < dist[t] <= EXTREME_BUY_DUMP_PREWINDOW_SLOTS
|
||||
and future_min < float(s.sell_price) - degrad_czk_kwh
|
||||
):
|
||||
out.add(t)
|
||||
return out
|
||||
|
||||
|
||||
def _slots_until_buy_le(
|
||||
slots: list[PlanningSlot],
|
||||
buy_thr: float,
|
||||
) -> list[int]:
|
||||
"""Počet slotů do nejbližšího buy <= thr (0 = v tomto slotu, T = nikdy)."""
|
||||
t_len = len(slots)
|
||||
dist = [t_len] * t_len
|
||||
next_idx = t_len
|
||||
for i in range(t_len - 1, -1, -1):
|
||||
if float(slots[i].buy_price) <= buy_thr:
|
||||
next_idx = i
|
||||
dist[i] = (next_idx - i) if next_idx < t_len else t_len
|
||||
return dist
|
||||
|
||||
|
||||
def _pre_negative_sell_export_window(
|
||||
slots: list[PlanningSlot],
|
||||
) -> tuple[int | None, int | None]:
|
||||
@@ -1138,6 +1207,8 @@ def solve_dispatch(
|
||||
if float(slots[i].buy_price) < 0.0:
|
||||
seen_neg_buy = True
|
||||
future_neg_buy_from[i] = seen_neg_buy
|
||||
future_extreme_buy_from = _future_extreme_buy_from(slots, buy_extreme_thr)
|
||||
dist_to_extreme_buy = _slots_until_buy_le(slots, buy_extreme_thr)
|
||||
|
||||
# EV proměnné per vozidlo
|
||||
ev_direct = [[pulp.LpVariable(f"evd_{e}_{t}", 0,
|
||||
@@ -1195,6 +1266,14 @@ def solve_dispatch(
|
||||
min_spread_pre = float(degradation_cost_effective)
|
||||
purchase_fixed_pre = _purchase_pricing_fixed(grid)
|
||||
fixed_tariff_like_pre = purchase_fixed_pre or _horizon_fixed_tariff_like(slots)
|
||||
neg_sell_bat_dump_slots = _neg_sell_bat_dump_slots(
|
||||
slots,
|
||||
operating_mode=om,
|
||||
purchase_fixed=purchase_fixed_pre,
|
||||
grid=grid,
|
||||
buy_extreme_thr=buy_extreme_thr,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
)
|
||||
profitable_export_ts_pre: set[int] = set()
|
||||
if om == "AUTO":
|
||||
for _t in range(T):
|
||||
@@ -1281,6 +1360,7 @@ def solve_dispatch(
|
||||
|
||||
peak_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
||||
pv_charge_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
||||
neg_sell_bat_dump_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
||||
neg_sell_soc_underfill: list[tuple[int, pulp.LpVariable]] = []
|
||||
fixed_tariff_like = fixed_tariff_like_pre
|
||||
block_export_neg_sell = bool(getattr(grid, "block_export_on_negative_sell", False))
|
||||
@@ -1352,6 +1432,14 @@ def solve_dispatch(
|
||||
float(battery.usable_capacity_wh),
|
||||
)
|
||||
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),
|
||||
)
|
||||
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))
|
||||
|
||||
# --- Účelová funkce (jen OTE sloty; terminal SoC shadow price na konci horizontu) ---
|
||||
# Kanály: gi×buy, −ge_pv×sell, −ge_bat×sell, +ge_bat×acquisition (export bat. jen v discharge slotách).
|
||||
@@ -1395,6 +1483,18 @@ def solve_dispatch(
|
||||
)
|
||||
else 0
|
||||
)
|
||||
+ (
|
||||
ge_pv[t]
|
||||
* NEG_SELL_PV_B_VENT_PENALTY_CZK_KWH
|
||||
* INTERVAL_H
|
||||
/ 1000
|
||||
if (
|
||||
om == "AUTO"
|
||||
and float(slots[t].sell_price) < 0.0
|
||||
and not purchase_fixed_pre
|
||||
)
|
||||
else 0
|
||||
)
|
||||
+ pulp.lpSum(
|
||||
ev_direct[e][t] * slots[t].buy_price * INTERVAL_H / 1000
|
||||
+ ev_via_bat[e][t] * slots[t].buy_price * EV_ROUNDTRIP_FACTOR * INTERVAL_H / 1000
|
||||
@@ -1445,6 +1545,10 @@ def solve_dispatch(
|
||||
us * NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH
|
||||
for _t, us in neg_sell_soc_underfill
|
||||
)
|
||||
+ pulp.lpSum(
|
||||
sf * NEG_SELL_BAT_DUMP_SHORTFALL_PENALTY_CZK_KWH * INTERVAL_H / 1000.0
|
||||
for _t, sf, _cap in neg_sell_bat_dump_shortfall
|
||||
)
|
||||
+ pulp.lpSum(
|
||||
-25.0 * z_export[t]
|
||||
for t in range(T)
|
||||
@@ -1457,6 +1561,8 @@ def solve_dispatch(
|
||||
prob += sf >= cap_w - ge_bat[t_sf]
|
||||
for t_sf, sf, cap_w in pv_charge_shortfall:
|
||||
prob += sf >= cap_w - bc_pv[t_sf]
|
||||
for t_sf, sf, cap_w in neg_sell_bat_dump_shortfall:
|
||||
prob += sf >= cap_w - ge_bat[t_sf]
|
||||
for t_us, us in neg_sell_soc_underfill:
|
||||
prob += us >= float(battery.soc_max_wh) - soc[t_us]
|
||||
preneg_export_min_soc_wh = float(min_soc_wh) + max(
|
||||
@@ -1602,13 +1708,16 @@ def solve_dispatch(
|
||||
+ heat_pump.rated_heating_power_w,
|
||||
)
|
||||
|
||||
# Záporný prodej (sell < 0): baterii v tomhle okně nevybíjíme (dump má proběhnout předtím).
|
||||
# Export v okně sell<0 může vzniknout jen z přebytku FVE (pv_a/pv_b). Výjimka: EV-via-battery
|
||||
# (pokud by bylo připojené a požadovalo výkon) – to kryjeme přes bd >= ev_via_bat.
|
||||
# Záporný prodej (sell < 0): výboj baterie jen před extrémně záporným buy (v11).
|
||||
# Export FVE při sell<0: spot = nabíjení/curtail A; ventil jen pole B při plné baterii.
|
||||
if s.sell_price < 0:
|
||||
prob += w_arb[t] == 0
|
||||
prob += bd[t] <= pulp.lpSum(ev_via_bat[e][t] for e in range(EV))
|
||||
prob += ge_bat[t] == 0
|
||||
block_neg_sell_export_t = bool(
|
||||
getattr(grid, "block_export_on_negative_sell", False)
|
||||
)
|
||||
if t not in neg_sell_bat_dump_slots:
|
||||
prob += ge_bat[t] == 0
|
||||
ev_cap_neg = sum(
|
||||
float(vehicles[e].max_charge_power_w)
|
||||
for e in range(EV)
|
||||
@@ -1640,32 +1749,25 @@ def solve_dispatch(
|
||||
if block_pv_export_neg_sell:
|
||||
prob += ge_pv[t] == 0
|
||||
# Tvrdý zákaz vývozu jen při block_export_on_negative_sell (KV1).
|
||||
# GEN cut-off (z_gen_cutoff) nesmí vynutit ge==0 — jinak nelze odvést pole B při plné baterii (BA81).
|
||||
block_neg_sell_export = bool(
|
||||
getattr(grid, "block_export_on_negative_sell", False)
|
||||
)
|
||||
if block_neg_sell_export:
|
||||
if block_neg_sell_export_t:
|
||||
prob += ge[t] == 0
|
||||
prob += ge_pv[t] == 0
|
||||
prob += ge_bat[t] == 0
|
||||
elif purchase_fixed_pre:
|
||||
# Fixní nákup + spot výkup (BA81, KV1 bez block_export): sell<0 = platíš za vývoz.
|
||||
# Nesouvisí s NT/VT skokem buy — řídí se výkupní cenou, ne rozptylem buy v horizontu.
|
||||
# Přebytek FVE → baterie / curtail A; B přes z_gen_cutoff nebo bc_pv.
|
||||
prob += ge[t] == 0
|
||||
prob += ge_pv[t] == 0
|
||||
elif not purchase_fixed_pre and pv_surplus_neg_w > 500:
|
||||
# Spot (home-01): při sell<0 neexportovat, dokud není baterie plná (curtailable A).
|
||||
# Dříve skip_pv_store_block + pv_b vynucoval export i při prázdné baterii.
|
||||
elif not purchase_fixed_pre:
|
||||
# Spot (home-01): ge_pv=0 dokud není plná baterie; pak jen ventil pole B (ne celý surplus).
|
||||
soc_prev_neg = current_soc_wh if t == 0 else soc[t - 1]
|
||||
w_pv_full = pulp.LpVariable(f"w_pv_full_neg_{t}", cat=pulp.LpBinary)
|
||||
w_pv_b_vent = pulp.LpVariable(f"w_pv_b_vent_neg_{t}", cat=pulp.LpBinary)
|
||||
m_soc_neg = float(battery.soc_max_wh)
|
||||
prob += soc_prev_neg >= (
|
||||
float(battery.soc_max_wh)
|
||||
m_soc_neg
|
||||
- soc_headroom_wh
|
||||
- float(battery.soc_max_wh) * (1 - w_pv_full)
|
||||
- m_soc_neg * (1 - w_pv_b_vent)
|
||||
)
|
||||
prob += ge_pv[t] <= float(pv_surplus_neg_w) * w_pv_full
|
||||
prob += ge[t] <= float(grid.max_export_power_w) * w_pv_full
|
||||
prob += ge_pv[t] <= float(s.pv_b_forecast_w) * w_pv_b_vent
|
||||
|
||||
soc_prev_expr = current_soc_wh if t == 0 else soc[t - 1]
|
||||
arb_t = arb_floor_series[t]
|
||||
@@ -1712,8 +1814,11 @@ def solve_dispatch(
|
||||
# Významný export z baterie ⇒ koncové SoC ≥ podlaha (FVE export ge_pv bez této podlahy).
|
||||
m_ge = float(grid.max_export_power_w)
|
||||
m_soc_bigm = float(battery.usable_capacity_wh)
|
||||
prob += ge_bat[t] <= m_ge * z_export[t]
|
||||
prob += ge_bat[t] >= GE_MIN_EXPORT_W * z_export[t]
|
||||
if t in neg_sell_bat_dump_slots:
|
||||
prob += ge_bat[t] <= m_ge
|
||||
else:
|
||||
prob += ge_bat[t] <= m_ge * z_export[t]
|
||||
prob += ge_bat[t] >= GE_MIN_EXPORT_W * z_export[t]
|
||||
# Bez hluboké relaxace: export končí ≥ rezerva. Při hluboké relaxaci (soc_panel_min pod min_soc)
|
||||
# sladit s LP spodkem — jinak z_export vynutil arb_base a blokoval vývoz k planner floor.
|
||||
if (
|
||||
@@ -1810,7 +1915,7 @@ def solve_dispatch(
|
||||
prob += bc_pv[t] == 0
|
||||
else:
|
||||
prob += bc_pv[t] <= float(pv_surplus_w)
|
||||
if t not in discharge_export_slots:
|
||||
if t not in discharge_export_slots and t not in neg_sell_bat_dump_slots:
|
||||
prob += ge_bat[t] == 0
|
||||
prob += z_export[t] == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user