fix max sell z baterky
This commit is contained in:
@@ -68,10 +68,11 @@ 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-28-pre-neg-buy-soc-phases-v25"
|
||||
PLANNER_BUILD_TAG = "2026-05-28-evening-peak-full-export-v26"
|
||||
POS_SELL_PRE_NEG_SOC_SHORTFALL_PENALTY_CZK_PER_WH = 0.30
|
||||
PRE_NEG_BUY_SOC_CEILING_SLACK_PENALTY_CZK_PER_WH = 0.25
|
||||
PRE_NEG_BUY_EMPTY_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
||||
EVENING_PEAK_SELL_EPS_CZK_KWH = 0.05
|
||||
# buy<0: preferovat import před PV A→bat (měkké; tvrdé bc_pv=0 láme bilanci s polem B).
|
||||
PRE_NEG_BUY_PV_CHARGE_PENALTY_CZK_KWH = 250.0
|
||||
CORRECTION_WINDOW_H = 1 # hodina zpět pro výpočet korekčního faktoru
|
||||
@@ -811,6 +812,47 @@ def _battery_export_cap_w(battery: Any, grid: Any) -> float:
|
||||
)
|
||||
|
||||
|
||||
def _evening_push_battery_export_w(
|
||||
slot: PlanningSlot,
|
||||
battery: Any,
|
||||
grid: Any,
|
||||
) -> float:
|
||||
"""
|
||||
Nejvyšší ge_bat v push slotu při load-first: bd+ge_bat ≤ max_discharge, gi ≤ load+bc_gi.
|
||||
Prakticky max export z baterie ≈ min(site/inverter cap, max_discharge − load).
|
||||
"""
|
||||
cap = _battery_export_cap_w(battery, grid)
|
||||
load_w = max(0.0, float(slot.load_baseline_w))
|
||||
discharge_headroom = max(
|
||||
0.0,
|
||||
float(battery.max_discharge_power_w) - load_w,
|
||||
)
|
||||
return min(cap, discharge_headroom)
|
||||
|
||||
|
||||
def _dispatch_grid_setpoint_w(
|
||||
*,
|
||||
gi_w: float,
|
||||
ge_w: float,
|
||||
ge_bat_w: float,
|
||||
ge_pv_w: float,
|
||||
max_export_power_w: int,
|
||||
) -> tuple[int, str]:
|
||||
"""
|
||||
grid_setpoint pro export do sítě (záporný W) a export_mode.
|
||||
gi−ge může být ~0 při load-first, i když ge_bat exportuje — Deye reg 143 potřebuje |grid_setpoint|.
|
||||
"""
|
||||
ge_total = max(0.0, float(ge_w))
|
||||
ge_bat_v = max(0.0, float(ge_bat_w))
|
||||
cap = float(max_export_power_w)
|
||||
if ge_bat_v >= GE_MIN_EXPORT_W:
|
||||
export_w = min(cap, max(ge_total, ge_bat_v + max(0.0, float(ge_pv_w))))
|
||||
return -int(round(export_w)), "BATTERY_SELL"
|
||||
if ge_total >= GE_MIN_EXPORT_W:
|
||||
return -int(round(min(cap, ge_total))), "PV_SURPLUS"
|
||||
return round(float(gi_w) - ge_total), "NONE"
|
||||
|
||||
|
||||
def _prague_hour(slot: PlanningSlot) -> int:
|
||||
dt = slot.interval_start
|
||||
if dt.tzinfo is None:
|
||||
@@ -972,8 +1014,11 @@ def _evening_battery_export_push_indices(
|
||||
evening_start_hour: int = 17,
|
||||
) -> list[int]:
|
||||
"""
|
||||
Tvrdý push ge_bat u večerních peak slotů (profitable ∩ pásmo ≥17:00 − degrad).
|
||||
Počet slotů = kolik jich unese rozpočet Wh (ne pevné top-3 / ≥2 sloty).
|
||||
Večerní push: plný ge_bat na top sell sloty (≥17h Prague).
|
||||
|
||||
Ne jeden slot — kolik slotů unese Wh rozpočet (v24), seřazených sell desc.
|
||||
Kandidáti jen u denního večerního max − EVENING_PEAK_SELL_EPS (úzké pásmo),
|
||||
ne celé široké peak−degrad. Ráno / odpoledne řeší jiné větve solveru.
|
||||
"""
|
||||
if per_slot_discharge_wh <= 0.0:
|
||||
return []
|
||||
@@ -983,6 +1028,14 @@ def _evening_battery_export_push_indices(
|
||||
evening_start_hour=evening_start_hour,
|
||||
)
|
||||
candidates = [t for t in peak_ts if t in profitable_export_ts]
|
||||
if not candidates:
|
||||
return []
|
||||
max_sell = max(float(slots[t].sell_price) for t in candidates)
|
||||
candidates = [
|
||||
t
|
||||
for t in candidates
|
||||
if float(slots[t].sell_price) >= max_sell - EVENING_PEAK_SELL_EPS_CZK_KWH
|
||||
]
|
||||
if not candidates:
|
||||
return []
|
||||
push_budget_wh = _evening_push_discharge_budget_wh(
|
||||
@@ -1491,6 +1544,48 @@ def solve_dispatch(
|
||||
fixed_tariff=fixed_tariff_like_pre,
|
||||
):
|
||||
profitable_export_ts_pre.add(_t)
|
||||
evening_push_ts: set[int] = set()
|
||||
evening_early_export_penalty_ts: set[int] = set()
|
||||
if om == "AUTO":
|
||||
per_slot_discharge_wh_pre = max(
|
||||
float(battery.max_discharge_power_w)
|
||||
* float(battery.discharge_efficiency)
|
||||
* INTERVAL_H,
|
||||
0.0,
|
||||
)
|
||||
discharge_buf_pre = float(getattr(battery, "discharge_slot_buffer", 0) or 0)
|
||||
evening_push_ts = set(
|
||||
_evening_battery_export_push_indices(
|
||||
slots,
|
||||
profitable_export_ts=profitable_export_ts_pre,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
current_soc_wh=float(current_soc_wh),
|
||||
min_soc_wh=float(min_soc_wh),
|
||||
soc_max_wh=float(battery.soc_max_wh),
|
||||
per_slot_discharge_wh=per_slot_discharge_wh_pre,
|
||||
discharge_slot_buffer=discharge_buf_pre,
|
||||
)
|
||||
)
|
||||
max_evening_sell_by_day: dict[object, float] = {}
|
||||
for t_ev, s_ev in enumerate(slots):
|
||||
if _prague_hour(s_ev) < 17:
|
||||
continue
|
||||
d_ev = _prague_calendar_date(s_ev)
|
||||
max_evening_sell_by_day[d_ev] = max(
|
||||
max_evening_sell_by_day.get(d_ev, 0.0),
|
||||
float(s_ev.sell_price),
|
||||
)
|
||||
for t_ev, s_ev in enumerate(slots):
|
||||
if _prague_hour(s_ev) < 17:
|
||||
continue
|
||||
if t_ev not in profitable_export_ts_pre or t_ev not in discharge_export_slots:
|
||||
continue
|
||||
if t_ev in evening_push_ts:
|
||||
continue
|
||||
d_ev = _prague_calendar_date(s_ev)
|
||||
peak_sell = max_evening_sell_by_day.get(d_ev, 0.0)
|
||||
if float(s_ev.sell_price) < peak_sell - EVENING_PEAK_SELL_EPS_CZK_KWH:
|
||||
evening_early_export_penalty_ts.add(t_ev)
|
||||
last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy(
|
||||
slots, first_neg_buy_idx
|
||||
)
|
||||
@@ -1604,6 +1699,8 @@ def solve_dispatch(
|
||||
for t in range(T):
|
||||
if t not in discharge_export_slots:
|
||||
continue
|
||||
if t in evening_push_ts:
|
||||
continue
|
||||
if not _slot_profitable_battery_export(
|
||||
slots[t],
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
@@ -1849,6 +1946,10 @@ def solve_dispatch(
|
||||
for t in range(T)
|
||||
if t in discharge_export_slots and t in profitable_export_ts_pre
|
||||
)
|
||||
+ pulp.lpSum(
|
||||
-250.0 * z_export[t]
|
||||
for t in evening_push_ts
|
||||
)
|
||||
)
|
||||
|
||||
# --- Omezení ---
|
||||
@@ -1892,22 +1993,17 @@ def solve_dispatch(
|
||||
for t_empty in pre_neg_buy_empty_ts:
|
||||
if t_empty in discharge_export_slots:
|
||||
prob += ge_bat[t_empty] >= export_push_w * z_export[t_empty]
|
||||
discharge_buf = float(getattr(battery, "discharge_slot_buffer", 0) or 0)
|
||||
evening_push_ts = _evening_battery_export_push_indices(
|
||||
slots,
|
||||
profitable_export_ts=profitable_export_ts,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
current_soc_wh=float(current_soc_wh),
|
||||
min_soc_wh=float(min_soc_wh),
|
||||
soc_max_wh=float(battery.soc_max_wh),
|
||||
per_slot_discharge_wh=per_slot_discharge_wh,
|
||||
discharge_slot_buffer=discharge_buf,
|
||||
)
|
||||
for t_peak in evening_push_ts:
|
||||
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
|
||||
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
|
||||
# Ostatní profitable sloty: shortfall penalizace (ne tvrdý push na celý horizont).
|
||||
push_floor_w = _evening_push_battery_export_w(
|
||||
slots[t_peak], battery, grid
|
||||
)
|
||||
if push_floor_w >= GE_MIN_EXPORT_W:
|
||||
prob += ge_bat[t_peak] >= push_floor_w * z_export[t_peak]
|
||||
# Ostatní profitable sloty: měkká shortfall penalizace (ne večerní push).
|
||||
if (
|
||||
last_pos_sell_pre_neg_buy is not None
|
||||
and pos_sell_soc_shortfall is not None
|
||||
@@ -2516,25 +2612,24 @@ def solve_dispatch(
|
||||
hp_on = hp_raw > heat_pump.rated_heating_power_w * 0.3
|
||||
bc_tot = float(pulp.value(bc_pv[t]) or 0) + float(pulp.value(bc_gi[t]) or 0)
|
||||
batt_w = round(bc_tot - float(pulp.value(bd[t]) or 0))
|
||||
grid_w = round(pulp.value(gi[t]) - pulp.value(ge[t]))
|
||||
ge_bat_w = round(float(pulp.value(ge_bat[t]) or 0))
|
||||
ge_pv_w = round(float(pulp.value(ge_pv[t]) or 0))
|
||||
grid_w, export_mode = _dispatch_grid_setpoint_w(
|
||||
gi_w=float(pulp.value(gi[t]) or 0),
|
||||
ge_w=float(pulp.value(ge[t]) or 0),
|
||||
ge_bat_w=float(ge_bat_w),
|
||||
ge_pv_w=float(ge_pv_w),
|
||||
max_export_power_w=int(grid.max_export_power_w),
|
||||
)
|
||||
soc_pct = round(pulp.value(soc[t]) / battery.usable_capacity_wh * 100, 1)
|
||||
export_limit_w = int(grid.max_export_power_w) if grid_w < 0 else 0
|
||||
ge_bat_w = round(float(pulp.value(ge_bat[t]) or 0))
|
||||
export_mode = "NONE"
|
||||
if grid_w < 0:
|
||||
export_mode = (
|
||||
"BATTERY_SELL"
|
||||
if ge_bat_w >= GE_MIN_EXPORT_W
|
||||
else "PV_SURPLUS"
|
||||
)
|
||||
|
||||
# Deye: default PASSIVE (střídač pokryje load). CHARGE/SELL jen v maskovaných AUTO slotech.
|
||||
deye_mode = "PASSIVE"
|
||||
if om == "AUTO":
|
||||
if (
|
||||
slots[t].allow_discharge_export
|
||||
and batt_w < 0
|
||||
and grid_w < 0
|
||||
and ge_bat_w >= GE_MIN_EXPORT_W
|
||||
):
|
||||
deye_mode = "SELL"
|
||||
elif slots[t].allow_charge and batt_w > 0 and grid_w > 0:
|
||||
|
||||
Reference in New Issue
Block a user