tune microcycling
This commit is contained in:
@@ -149,6 +149,93 @@ def _prague_dow_hour(interval_start: datetime) -> tuple[int, int]:
|
||||
return (loc.weekday() + 1) % 7, loc.hour
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Slot pre-selection (anti-micro-cycling)
|
||||
# ============================================================
|
||||
|
||||
def _select_charge_slots(
|
||||
slots: list["PlanningSlot"],
|
||||
battery,
|
||||
current_soc_wh: float,
|
||||
) -> set[int]:
|
||||
"""
|
||||
Pre-select which slots are eligible for battery charging.
|
||||
Only the X cheapest sell-price PV-surplus slots are selected,
|
||||
enough to fill the battery with a configurable buffer.
|
||||
Returns set of slot indices. Empty set = no restriction.
|
||||
"""
|
||||
charge_buf = float(getattr(battery, "charge_slot_buffer", 0) or 0)
|
||||
if charge_buf <= 0:
|
||||
return set(range(len(slots)))
|
||||
|
||||
energy_to_fill = float(battery.soc_max_wh) - float(current_soc_wh)
|
||||
if energy_to_fill <= 0:
|
||||
return set()
|
||||
|
||||
candidates: list[tuple[int, float, float]] = []
|
||||
for t, s in enumerate(slots):
|
||||
pv_surplus = max(0, s.pv_a_forecast_w + s.pv_b_forecast_w - s.load_baseline_w)
|
||||
if pv_surplus <= 0:
|
||||
continue
|
||||
charge_w = min(float(battery.max_charge_power_w), float(pv_surplus))
|
||||
charge_wh = charge_w * float(battery.charge_efficiency) * INTERVAL_H
|
||||
candidates.append((t, float(s.sell_price), charge_wh))
|
||||
|
||||
candidates.sort(key=lambda x: x[1])
|
||||
|
||||
selected: set[int] = set()
|
||||
cumulative = 0.0
|
||||
target = energy_to_fill * charge_buf
|
||||
for t, _price, wh in candidates:
|
||||
if cumulative >= target:
|
||||
break
|
||||
selected.add(t)
|
||||
cumulative += wh
|
||||
|
||||
if cumulative < energy_to_fill:
|
||||
selected = set(c[0] for c in candidates)
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def _select_discharge_export_slots(
|
||||
slots: list["PlanningSlot"],
|
||||
battery,
|
||||
) -> set[int]:
|
||||
"""
|
||||
Pre-select which slots may use battery energy for grid export.
|
||||
Only the Y most expensive sell-price slots are selected,
|
||||
enough to empty the exportable portion of the battery with a buffer.
|
||||
Returns set of slot indices. Empty set = no restriction.
|
||||
"""
|
||||
discharge_buf = float(getattr(battery, "discharge_slot_buffer", 0) or 0)
|
||||
if discharge_buf <= 0:
|
||||
return set(range(len(slots)))
|
||||
|
||||
exportable = float(battery.soc_max_wh) - float(battery.min_soc_wh)
|
||||
if exportable <= 0:
|
||||
return set()
|
||||
|
||||
candidates = [(t, float(s.sell_price)) for t, s in enumerate(slots)]
|
||||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
energy_per_slot = (
|
||||
float(battery.max_discharge_power_w)
|
||||
* float(battery.discharge_efficiency)
|
||||
* INTERVAL_H
|
||||
)
|
||||
target = exportable * discharge_buf
|
||||
selected: set[int] = set()
|
||||
cumulative = 0.0
|
||||
for t, _price in candidates:
|
||||
if cumulative >= target:
|
||||
break
|
||||
selected.add(t)
|
||||
cumulative += energy_per_slot
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Datové třídy (lze nahradit pydantic modely)
|
||||
# ============================================================
|
||||
@@ -448,11 +535,24 @@ def solve_dispatch(
|
||||
|
||||
if price_failsafe_active:
|
||||
for t in range(T):
|
||||
# Fail-safe aplikujeme po slotech: v predikovaných cenách zakážeme pouze export.
|
||||
# Baterie se má dál normálně používat pro interní spotřebu (nabíjení/vybíjení do domu).
|
||||
if slots[t].is_predicted_price:
|
||||
prob += ge[t] == 0
|
||||
|
||||
# Slot pre-selection: omezení nabíjení a discharge-exportu na vybrané sloty
|
||||
if om == "AUTO":
|
||||
charge_slots = _select_charge_slots(slots, battery, current_soc_wh)
|
||||
discharge_export_slots = _select_discharge_export_slots(slots, battery)
|
||||
for t in range(T):
|
||||
if t not in charge_slots:
|
||||
prob += bc[t] == 0
|
||||
|
||||
if t not in discharge_export_slots:
|
||||
s = slots[t]
|
||||
ev_total_t = pulp.lpSum(
|
||||
ev_direct[e][t] + ev_via_bat[e][t] for e in range(EV)
|
||||
)
|
||||
prob += bd[t] <= s.load_baseline_w + ev_total_t + hp[t]
|
||||
|
||||
# Deadline constraints pro EV
|
||||
for e, session in enumerate(ev_sessions):
|
||||
if session and session.target_deadline and session.energy_needed_wh > 0:
|
||||
@@ -795,6 +895,8 @@ async def _load_site_context(site_id: int, db):
|
||||
ab.charge_efficiency,
|
||||
ab.discharge_efficiency,
|
||||
ab.degradation_cost_czk_kwh,
|
||||
ab.charge_slot_buffer,
|
||||
ab.discharge_slot_buffer,
|
||||
LEAST(
|
||||
COALESCE(ai.max_battery_charge_w, ai.max_charge_power_w),
|
||||
COALESCE(
|
||||
@@ -856,6 +958,8 @@ async def _load_site_context(site_id: int, db):
|
||||
degradation_cost_czk_kwh=float(brow["degradation_cost_czk_kwh"]),
|
||||
max_charge_power_w=ec_i,
|
||||
max_discharge_power_w=ed_i,
|
||||
charge_slot_buffer=float(brow["charge_slot_buffer"]) if brow["charge_slot_buffer"] is not None else 0,
|
||||
discharge_slot_buffer=float(brow["discharge_slot_buffer"]) if brow["discharge_slot_buffer"] is not None else 0,
|
||||
)
|
||||
|
||||
hrow = await db.fetchrow(
|
||||
|
||||
Reference in New Issue
Block a user