dalsi
This commit is contained in:
@@ -668,25 +668,85 @@ def _prague_calendar_date(slot: PlanningSlot):
|
||||
return dt.astimezone(ZoneInfo("Europe/Prague")).date()
|
||||
|
||||
|
||||
MORNING_PRENEG_START_HOUR = 5
|
||||
MORNING_PRENEG_END_HOUR = 11
|
||||
NEGATIVE_BUY_GRID_CHARGE_MIN_W = 8_000.0
|
||||
PRENEG_MORNING_EXPORT_MIN_W = 5_000.0
|
||||
|
||||
|
||||
def _prague_hour(slot: PlanningSlot) -> int:
|
||||
dt = slot.interval_start
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(ZoneInfo("Europe/Prague")).hour
|
||||
|
||||
|
||||
def _morning_pre_neg_zone_peak_sell(
|
||||
slots: list[PlanningSlot],
|
||||
first_neg_sell_idx: int | None,
|
||||
) -> float | None:
|
||||
"""Max kladný sell v pásmu 5–11 Prague před prvním sell<0 (shodně s R__063)."""
|
||||
if first_neg_sell_idx is None or first_neg_sell_idx <= 0:
|
||||
return None
|
||||
neg_day = _prague_calendar_date(slots[first_neg_sell_idx])
|
||||
sells = [
|
||||
float(slots[i].sell_price)
|
||||
for i in range(first_neg_sell_idx)
|
||||
if float(slots[i].sell_price) >= 0.0
|
||||
and _prague_calendar_date(slots[i]) == neg_day
|
||||
and MORNING_PRENEG_START_HOUR <= _prague_hour(slots[i]) <= MORNING_PRENEG_END_HOUR
|
||||
]
|
||||
if not sells:
|
||||
return None
|
||||
return max(sells)
|
||||
|
||||
|
||||
def _pre_neg_peak_sell_idx(
|
||||
slots: list[PlanningSlot],
|
||||
first_neg_sell_idx: int | None,
|
||||
) -> int | None:
|
||||
"""Nejvyšší kladný sell před prvním sell<0 ve stejném kalendářním dni (Prague)."""
|
||||
"""Nejvyšší kladný sell v ranním pásmu před prvním sell<0 (ne půlnoc celého dne)."""
|
||||
if first_neg_sell_idx is None or first_neg_sell_idx <= 0:
|
||||
return None
|
||||
zone_peak = _morning_pre_neg_zone_peak_sell(slots, first_neg_sell_idx)
|
||||
if zone_peak is None:
|
||||
return None
|
||||
neg_day = _prague_calendar_date(slots[first_neg_sell_idx])
|
||||
positive = [
|
||||
(i, float(slots[i].sell_price))
|
||||
for i in range(first_neg_sell_idx)
|
||||
if float(slots[i].sell_price) >= 0.0
|
||||
and _prague_calendar_date(slots[i]) == neg_day
|
||||
and MORNING_PRENEG_START_HOUR <= _prague_hour(slots[i]) <= MORNING_PRENEG_END_HOUR
|
||||
]
|
||||
if not positive:
|
||||
return None
|
||||
return max(positive, key=lambda x: (x[1], x[0]))[0]
|
||||
|
||||
|
||||
def _morning_pre_neg_export_indices(
|
||||
slots: list[PlanningSlot],
|
||||
first_neg_sell_idx: int | None,
|
||||
*,
|
||||
degrad_czk_kwh: float,
|
||||
) -> list[int]:
|
||||
"""Všechny ranní peak sloty (sell ≥ zónový max − degrad) před prvním sell<0."""
|
||||
zone_peak = _morning_pre_neg_zone_peak_sell(slots, first_neg_sell_idx)
|
||||
if zone_peak is None or first_neg_sell_idx is None or first_neg_sell_idx <= 0:
|
||||
return []
|
||||
neg_day = _prague_calendar_date(slots[first_neg_sell_idx])
|
||||
out: list[int] = []
|
||||
for i in range(first_neg_sell_idx):
|
||||
if (
|
||||
float(slots[i].sell_price) >= zone_peak - degrad_czk_kwh
|
||||
and float(slots[i].sell_price) >= 0.0
|
||||
and _prague_calendar_date(slots[i]) == neg_day
|
||||
and MORNING_PRENEG_START_HOUR <= _prague_hour(slots[i]) <= MORNING_PRENEG_END_HOUR
|
||||
):
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
def _pv_forced_vent_export_allowed(
|
||||
t: int,
|
||||
*,
|
||||
@@ -972,8 +1032,20 @@ def solve_dispatch(
|
||||
# Slack penalizujeme v objective; samotné omezení přidáme až po definici soc.
|
||||
first_neg_sell_idx, pre_neg_export_last_t = _pre_negative_sell_export_window(slots)
|
||||
t_pre_neg_peak = _pre_neg_peak_sell_idx(slots, first_neg_sell_idx)
|
||||
morning_pre_neg_export_ts = _morning_pre_neg_export_indices(
|
||||
slots,
|
||||
first_neg_sell_idx,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
)
|
||||
if first_neg_sell_idx is not None and first_neg_sell_idx > 0 and floor_pct is not None:
|
||||
t_anchor = first_neg_sell_idx - 1
|
||||
# Kotva na ranním peaku (ne na posledním slotu před sell<0) — jinak dump až v 07:30.
|
||||
if (
|
||||
t_pre_neg_peak is not None
|
||||
and t_pre_neg_peak < first_neg_sell_idx - 1
|
||||
):
|
||||
t_anchor = t_pre_neg_peak
|
||||
else:
|
||||
t_anchor = first_neg_sell_idx - 1
|
||||
soc_anchor_slack = pulp.LpVariable("soc_anchor_slack_wh", 0, float(battery.usable_capacity_wh))
|
||||
|
||||
daytime_en = bool(getattr(battery, "planner_daytime_charge_target_enabled", True))
|
||||
@@ -1100,12 +1172,16 @@ def solve_dispatch(
|
||||
# --- Omezení ---
|
||||
for _t, sf, cap_w in peak_export_shortfall:
|
||||
prob += sf >= cap_w - ge[_t]
|
||||
if (
|
||||
om == "AUTO"
|
||||
and t_pre_neg_peak is not None
|
||||
and t_pre_neg_peak in discharge_export_slots
|
||||
):
|
||||
prob += ge_bat[t_pre_neg_peak] >= 5000.0 * z_export[t_pre_neg_peak]
|
||||
preneg_export_min_soc_wh = float(min_soc_wh) + max(
|
||||
float(battery.max_discharge_power_w)
|
||||
* float(battery.discharge_efficiency)
|
||||
* INTERVAL_H,
|
||||
1000.0,
|
||||
)
|
||||
if om == "AUTO":
|
||||
for t_peak in morning_pre_neg_export_ts:
|
||||
if t_peak in discharge_export_slots:
|
||||
prob += ge_bat[t_peak] >= PRENEG_MORNING_EXPORT_MIN_W * z_export[t_peak]
|
||||
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
|
||||
@@ -1173,12 +1249,12 @@ def solve_dispatch(
|
||||
# Měkký breaker cap: gi_over[t] >= max(0, gi[t] - breaker).
|
||||
prob += gi_over[t] >= gi[t] - float(grid.max_import_power_w)
|
||||
|
||||
# SoC kontinuita
|
||||
# SoC kontinuita (bd do domu i ge_bat do sítě vybíjí baterii)
|
||||
soc_prev = current_soc_wh if t == 0 else soc[t - 1]
|
||||
prob += soc[t] == (
|
||||
soc_prev
|
||||
+ (bc_pv[t] + bc_gi[t]) * battery.charge_efficiency * INTERVAL_H
|
||||
- bd[t] / battery.discharge_efficiency * INTERVAL_H
|
||||
- (bd[t] + ge_bat[t]) / battery.discharge_efficiency * INTERVAL_H
|
||||
)
|
||||
|
||||
sv = safety_vars[t]
|
||||
@@ -1306,7 +1382,20 @@ def solve_dispatch(
|
||||
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 soc_panel_min[t] < min_soc_wh - 1e-3:
|
||||
if (
|
||||
om == "AUTO"
|
||||
and first_neg_sell_idx is not None
|
||||
and t < first_neg_sell_idx
|
||||
and floor_pct is not None
|
||||
):
|
||||
export_soc_floor_t = float(planner_floor_effective_wh)
|
||||
elif (
|
||||
om == "AUTO"
|
||||
and t in morning_pre_neg_export_ts
|
||||
and floor_pct is not None
|
||||
):
|
||||
export_soc_floor_t = float(planner_floor_effective_wh)
|
||||
elif soc_panel_min[t] < min_soc_wh - 1e-3:
|
||||
export_soc_floor_t = float(soc_panel_min[t])
|
||||
else:
|
||||
export_soc_floor_t = float(arb_base_wh)
|
||||
@@ -1375,6 +1464,29 @@ def solve_dispatch(
|
||||
prob += ge_bat[t] == 0
|
||||
prob += z_export[t] == 0
|
||||
|
||||
# Záporný buy: minimální grid import (spot arbitráž), jen pokud není baterie prakticky plná.
|
||||
for t in range(T):
|
||||
if float(slots[t].buy_price) >= 0.0:
|
||||
continue
|
||||
load_t = float(slots[t].load_baseline_w)
|
||||
min_gi = min(
|
||||
gi_upper,
|
||||
load_t + NEGATIVE_BUY_GRID_CHARGE_MIN_W,
|
||||
load_t + float(battery.max_charge_power_w) * 0.9,
|
||||
)
|
||||
if min_gi <= load_t + 500.0:
|
||||
continue
|
||||
if t == 0:
|
||||
if current_soc_wh >= float(battery.soc_max_wh) - soc_headroom_wh - 500.0:
|
||||
continue
|
||||
prob += gi[t] >= min_gi
|
||||
else:
|
||||
z_neg_chg = pulp.LpVariable(f"z_neg_chg_{t}", cat="Binary")
|
||||
prob += soc[t - 1] <= float(battery.soc_max_wh) - soc_headroom_wh - 500.0 + float(
|
||||
battery.usable_capacity_wh
|
||||
) * (1 - z_neg_chg)
|
||||
prob += gi[t] >= min_gi * z_neg_chg
|
||||
|
||||
# Ekonomické guardy: ceny v objective nestačí proti maskám / terminal SoC.
|
||||
# Referenční buy jen z ne-záporných slotů: jinak jeden buy<0 v horizontu označí
|
||||
# téměř všechny sloty jako „drahé“ (gi=0 pro dům) → Infeasible (home-01).
|
||||
|
||||
Reference in New Issue
Block a user