prepsani s opusem dle planu
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-24 22:44:21 +02:00
parent 2d021b15c3
commit 8bef1c6da6
11 changed files with 720 additions and 16 deletions

View File

@@ -64,7 +64,7 @@ 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"
PLANNER_BUILD_TAG = "2026-05-27-self-consistent-grid-mask-v12"
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
@@ -357,6 +357,12 @@ class PlanningSlot:
#: 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
min_buy_before_cutoff_czk_kwh: float | None = None
pv_charge_wh_ahead: float | None = None
neg_buy_wh_ahead: float | None = None
grid_charge_suppressed_reason: str | None = None
#: Pomocny atribut pro green_bonus v planning_interval (Kc/slot); lite default 0.
green_bonus_czk_per_slot: float = 0.0
# Lookahead pro relax spodní meze SoC: až 36 h od indexu slotu (pevné OTE ceny v horizontu).
@@ -510,6 +516,10 @@ class DispatchResult:
effective_buy_price: float
effective_sell_price: float
is_predicted_price: bool # shodné s PlanningSlot (chybí OTE v efektivní ceně → fn_get_predicted_price)
cashflow_czk: float
battery_arbitrage_czk: float
penalty_czk: float
green_bonus_czk: float
# ============================================================
@@ -997,6 +1007,7 @@ def solve_dispatch_two_pass(
snap1["inputs"]["acquisition_pass2_czk_kwh"] = round(acq2, 6)
snap1["inputs"]["two_pass_enabled"] = True
snap1["inputs"]["two_pass_converged"] = True
snap1["inputs"]["two_pass_skipped"] = False
return results1, ms1, snap1
slots2 = _slots_with_charge_acquisition(slots, acq2)
@@ -1019,6 +1030,7 @@ def solve_dispatch_two_pass(
snap2["inputs"]["acquisition_pass2_czk_kwh"] = round(acq2, 6)
snap2["inputs"]["two_pass_enabled"] = True
snap2["inputs"]["two_pass_converged"] = False
snap2["inputs"]["two_pass_skipped"] = False
snap2["inputs"]["solver_duration_ms_pass1"] = ms1
return results2, ms1 + ms2, snap2
@@ -2128,10 +2140,59 @@ def solve_dispatch(
if z_gen_cutoff is not None:
deye_gen_cutoff = bool(round(float(pulp.value(z_gen_cutoff[t]) or 0)))
cost = (
cashflow_czk_t = (
pulp.value(gi[t]) * slots[t].buy_price * INTERVAL_H / 1000
- pulp.value(ge[t]) * slots[t].sell_price * INTERVAL_H / 1000
)
ge_bat_value = float(pulp.value(ge_bat[t]) or 0)
battery_arbitrage_czk_t = (
ge_bat_value
* (float(slots[t].sell_price) - float(charge_acquisition_czk_kwh))
* INTERVAL_H
/ 1000.0
)
penalty_terms_t = 0.0
for _tt, _sf, _cap in peak_export_shortfall:
if _tt == t:
penalty_terms_t += (
float(pulp.value(_sf) or 0.0)
* PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH
* INTERVAL_H
/ 1000.0
)
for _tt, _sf, _cap in pv_charge_shortfall:
if _tt == t:
penalty_terms_t += (
float(pulp.value(_sf) or 0.0)
* PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH
* INTERVAL_H
/ 1000.0
)
for _tt, _sf, _cap in neg_sell_bat_dump_shortfall:
if _tt == t:
penalty_terms_t += (
float(pulp.value(_sf) or 0.0)
* NEG_SELL_BAT_DUMP_SHORTFALL_PENALTY_CZK_KWH
* INTERVAL_H
/ 1000.0
)
for _tt, _us in neg_sell_soc_underfill:
if _tt == t:
penalty_terms_t += (
float(pulp.value(_us) or 0.0)
* NEG_SELL_SOC_UNDERFILL_PENALTY_CZK_PER_WH
)
sv_t = safety_vars[t]
if sv_t is not None:
penalty_terms_t += float(pulp.value(sv_t) or 0.0) * safety_pen_czk_per_wh[t]
for _tt, _cv, _prev in commit_lp:
if _tt == t:
penalty_terms_t += float(pulp.value(_cv) or 0.0) * INTERVAL_H / 1000.0 * commit_pen
penalty_terms_t += float(pulp.value(ca[t]) or 0.0) * CURTAILMENT_PENALTY
green_bonus_czk_t = float(
getattr(slots[t], "green_bonus_czk_per_slot", 0.0) or 0.0
)
cost = cashflow_czk_t
results.append(DispatchResult(
interval_start = slots[t].interval_start,
@@ -2155,6 +2216,10 @@ def solve_dispatch(
effective_buy_price = slots[t].buy_price,
effective_sell_price = slots[t].sell_price,
is_predicted_price = bool(slots[t].is_predicted_price),
cashflow_czk = round(cashflow_czk_t, 4),
battery_arbitrage_czk = round(battery_arbitrage_czk_t, 4),
penalty_czk = round(penalty_terms_t, 4),
green_bonus_czk = round(green_bonus_czk_t, 4),
))
sell_rank = sorted(range(T), key=lambda i: float(slots[i].sell_price), reverse=True)[: min(3, T)]
@@ -2230,6 +2295,18 @@ def solve_dispatch(
"safety_deficit_wh": sdv,
"commitment_shortfall_w": cshort,
"commitment_penalty_czk_kwh": float(commit_pen) if cshort is not None else None,
"acquisition_used_czk_kwh": float(charge_acquisition_czk_kwh),
"grid_charge_suppressed_reason": getattr(
st, "grid_charge_suppressed_reason", None
),
"pv_charge_wh_ahead": float(
getattr(st, "pv_charge_wh_ahead", 0.0) or 0.0
),
"min_buy_before_cutoff_czk_kwh": (
float(st.min_buy_before_cutoff_czk_kwh)
if getattr(st, "min_buy_before_cutoff_czk_kwh", None) is not None
else None
),
}
)
night0 = slots[0]
@@ -2844,7 +2921,9 @@ async def _load_slots(
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,
charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at
charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at,
min_buy_before_cutoff_czk_kwh, pv_charge_wh_ahead, neg_buy_wh_ahead,
grid_charge_suppressed_reason
from ems.fn_load_planning_slots_full(
$1::int, $2::timestamptz, $3::timestamptz, $4::numeric
)
@@ -2882,6 +2961,12 @@ async def _load_slots(
d, "charge_acquisition_buy_czk_kwh"
),
charge_acquisition_cutoff_at=d.get("charge_acquisition_cutoff_at"),
min_buy_before_cutoff_czk_kwh=_slot_float_nullable(
d, "min_buy_before_cutoff_czk_kwh"
),
pv_charge_wh_ahead=_slot_float_nullable(d, "pv_charge_wh_ahead"),
neg_buy_wh_ahead=_slot_float_nullable(d, "neg_buy_wh_ahead"),
grid_charge_suppressed_reason=d.get("grid_charge_suppressed_reason"),
)
)
if not out:
@@ -2960,6 +3045,10 @@ async def _save_planning_run(
"heat_pump_setpoint_w": r.heat_pump_setpoint_w,
"pv_a_curtailed_w": r.pv_a_curtailed_w,
"expected_cost_czk": float(r.expected_cost_czk),
"cashflow_czk": float(r.cashflow_czk),
"battery_arbitrage_czk": float(r.battery_arbitrage_czk),
"penalty_czk": float(r.penalty_czk),
"green_bonus_czk": float(r.green_bonus_czk),
"effective_buy_price": float(r.effective_buy_price),
"effective_sell_price": float(r.effective_sell_price),
"is_predicted_price": r.is_predicted_price,