uprava vypoctu slotu
Some checks failed
CI and deploy / migration-check (push) Failing after 16s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-21 12:36:03 +02:00
parent 08f1b6741a
commit b78597fdda
8 changed files with 346 additions and 6 deletions

View File

@@ -335,6 +335,9 @@ class PlanningSlot:
future_avoided_buy_czk_kwh: float | None = None
future_sell_opportunity_czk_kwh: float | None = None
is_daytime_pv_surplus_slot: bool = False
#: Vážená nákupní / opportunity cena zásoby před prvním exportním oknem (SQL odhad z masek).
charge_acquisition_buy_czk_kwh: float | None = None
charge_acquisition_cutoff_at: datetime | None = None
# Lookahead pro relax spodní meze SoC: až 36 h od indexu slotu (pevné OTE ceny v horizontu).
@@ -759,6 +762,13 @@ def solve_dispatch(
avg_buy_terminal * terminal_factor / 1000.0
)
charge_acq_raw = getattr(slots[0], "charge_acquisition_buy_czk_kwh", None)
charge_acquisition_czk_kwh = (
float(charge_acq_raw)
if charge_acq_raw is not None
else min(float(s.buy_price) for s in slots)
)
# Kotva: poslední slot před prvním sell<0 by měl končit u planner floor (pokud relaxace existuje).
# Slack penalizujeme v objective; samotné omezení přidáme až po definici soc.
first_neg_sell_idx = next((i for i, s in enumerate(slots) if float(s.sell_price) < 0), None)
@@ -813,6 +823,8 @@ def solve_dispatch(
commit_lp.append((t, cv, cap_prev))
# --- Účelová funkce (jen OTE sloty; terminal SoC shadow price na konci horizontu) ---
# Arbitráž baterie: ge_bat v exportních slotech + charge_acquisition (SQL, před 1. exportem).
# Viz docs/04-modules/planning-arbitrage-accounting.md — ne stejnoslotové buy/sell.
prob += (
pulp.lpSum(
gi[t] * slots[t].buy_price * INTERVAL_H / 1000
@@ -829,6 +841,11 @@ def solve_dispatch(
)
+ gi_over[t] * IMPORT_OVER_BREAKER_PENALTY_CZK_KWH * INTERVAL_H / 1000
+ 0.5 * (bc[t] + bd[t]) * degradation_cost_effective * INTERVAL_H / 1000
+ (
ge_bat[t] * charge_acquisition_czk_kwh * INTERVAL_H / 1000
if om == "AUTO" and t in discharge_export_slots
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
@@ -1309,6 +1326,12 @@ def solve_dispatch(
"planner_daytime_charge_target_enabled": daytime_en,
"planner_charge_commitment_penalty_czk_kwh": float(commit_pen),
},
"charge_acquisition_buy_czk_kwh": charge_acquisition_czk_kwh,
"charge_acquisition_cutoff_at": (
slots[0].charge_acquisition_cutoff_at.isoformat()
if slots[0].charge_acquisition_cutoff_at is not None
else None
),
},
"masks": masks_snap,
"soc_bounds": soc_bounds_snap,
@@ -1872,7 +1895,8 @@ async def _load_slots(
ev1_connected, ev2_connected, allow_charge, allow_discharge_export,
night_baseload_target_wh, night_baseload_buffer_wh, safety_soc_target_wh,
future_avoided_buy_czk_kwh, future_sell_opportunity_czk_kwh,
is_daytime_pv_surplus_slot
is_daytime_pv_surplus_slot,
charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at
from ems.fn_load_planning_slots_full(
$1::int, $2::timestamptz, $3::timestamptz, $4::numeric
)
@@ -1906,6 +1930,10 @@ async def _load_slots(
d, "future_sell_opportunity_czk_kwh"
),
is_daytime_pv_surplus_slot=bool(d.get("is_daytime_pv_surplus_slot", False)),
charge_acquisition_buy_czk_kwh=_slot_float_nullable(
d, "charge_acquisition_buy_czk_kwh"
),
charge_acquisition_cutoff_at=d.get("charge_acquisition_cutoff_at"),
)
)
if not out: