From 4875c31338b526ae02e65168551ec69d6806c52e Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Sun, 26 Apr 2026 19:55:35 +0200 Subject: [PATCH] uprava solver gneport cutoff u ba81 --- .idea/data_source_mapping.xml | 1 + backend/services/planning_engine.py | 38 ++++++++++++++++++++++++----- docs/04-modules/operating-modes.md | 2 +- docs/04-modules/planning.md | 11 ++++++--- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml index 36fbe48..b1447d9 100644 --- a/.idea/data_source_mapping.xml +++ b/.idea/data_source_mapping.xml @@ -2,6 +2,7 @@ + diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index bdde503..5c3be60 100644 --- a/backend/services/planning_engine.py +++ b/backend/services/planning_engine.py @@ -551,6 +551,18 @@ def solve_dispatch( else None ) + om = (operating_mode or "AUTO").strip().upper() + # SELF_SUSTAIN dřív vynucoval ge[t] == 0, což umí udělat MILP infeasible v okamžiku, kdy: + # - baterie je na max SoC (nelze nabíjet), + # - PV pole B není curtailable, + # - a pv_b_forecast_w > load_baseline_w (typicky po ručním snížení baseline). + # Export v SELF_SUSTAIN proto povolíme jako nouzový ventil, ale silně penalizujeme, + # aby k němu docházelo jen když už neexistuje jiné fyzikálně možné řešení. + SELF_SUSTAIN_EXPORT_PENALTY_CZK_KWH = 100.0 + # Penalizace vypnutí GEN portu (mikroinvertory): preferujeme nechat zapnuto a vypnout jen když + # by to jinak vedlo k nežádoucímu exportu / infeasible řešení. + GEN_CUTOFF_PENALTY_CZK_KWH = 5.0 + # EV proměnné per vozidlo ev_direct = [[pulp.LpVariable(f"evd_{e}_{t}", 0, min(vehicles[e].max_charge_power_w, grid.max_import_power_w)) @@ -582,6 +594,16 @@ def solve_dispatch( pulp.lpSum( gi[t] * slots[t].buy_price * INTERVAL_H / 1000 - ge[t] * slots[t].sell_price * INTERVAL_H / 1000 + + ( + ge[t] * SELF_SUSTAIN_EXPORT_PENALTY_CZK_KWH * INTERVAL_H / 1000 + if om == "SELF_SUSTAIN" + else 0 + ) + + ( + (slots[t].pv_b_forecast_w * z_gen_cutoff[t]) * GEN_CUTOFF_PENALTY_CZK_KWH * INTERVAL_H / 1000 + if z_gen_cutoff is not None + else 0 + ) + gi_over[t] * IMPORT_OVER_BREAKER_PENALTY_CZK_KWH * INTERVAL_H / 1000 + 0.5 * (bc[t] + bd[t]) * degradation_cost_effective * INTERVAL_H / 1000 + pulp.lpSum( @@ -638,10 +660,16 @@ def solve_dispatch( # ev_via_bat kryto z discharge prob += pulp.lpSum(ev_via_bat[e][t] for e in range(EV)) <= bd[t] - # Záporná prodejní cena: export nepovinně zakazovat — účelovka už obsahuje -ge*sell - # (záporné sell zvyšuje náklad exportu). GEN cut-off držíme vypnutý (jinak by z_gen_cutoff - # uměle nulovalo forecast pole B). - if z_gen_cutoff is not None: + # GEN port cut-off chceme vůbec připustit jen v režimech/politikách, kde má smysl: + # - SELF_SUSTAIN (no-export intent; typicky ge=0, takže cut-off je bezpečnostní ventil), + # - BLOCK_EXPORT okna (v projektu reprezentované sloty se sell_price < 0), + # - případně explicitní no_export politika (pokud bude v kontextu dostupná). + allow_gen_cutoff = ( + om == "SELF_SUSTAIN" + or float(s.sell_price) < 0 + or bool(getattr(grid, "no_export", False)) + ) + if z_gen_cutoff is not None and not allow_gen_cutoff: prob += z_gen_cutoff[t] == 0 # Záporná nákupní cena → cap import (baseline domu + akumulace + řízené zátěže) @@ -703,10 +731,8 @@ def solve_dispatch( else: prob += ev_direct[e][t] + ev_via_bat[e][t] <= vehicles[e].max_charge_power_w - om = (operating_mode or "AUTO").strip().upper() if om == "SELF_SUSTAIN": for t in range(T): - prob += ge[t] == 0 prob += gi[t] <= slots[t].load_baseline_w elif om == "PRESERVE": for t in range(T): diff --git a/docs/04-modules/operating-modes.md b/docs/04-modules/operating-modes.md index ec14a81..0769e8d 100644 --- a/docs/04-modules/operating-modes.md +++ b/docs/04-modules/operating-modes.md @@ -12,7 +12,7 @@ | Mode | Solver constraints | Deye fyzický režim | Baterie | |------|-------------------|-------------------|---------| | AUTO | žádné | PASSIVE/SELL/CHARGE dle plánu | dle plánu | -| SELF_SUSTAIN | no_export, min_import | vždy PASSIVE | plné limity | +| SELF_SUSTAIN | min_import; export jen jako nouzový ventil (silně penalizovaný) | vždy PASSIVE | plné limity | | CHARGE_CHEAP | no_export, no_discharge | CHARGE | nabíjení max | | PRESERVE | no_charge, no_discharge | PASSIVE | lock (0/0) | | MANUAL | solver neběží | EMS nezapisuje | — | diff --git a/docs/04-modules/planning.md b/docs/04-modules/planning.md index 25df299..b8be583 100644 --- a/docs/04-modules/planning.md +++ b/docs/04-modules/planning.md @@ -267,12 +267,17 @@ kde: - `batt_charge_cap_w` = min(`battery.max_charge_power_w`, \((soc_{max}-soc)_{wh} / 0.25h\)) – tj. výkonově omezené a SoC-headroom omezené - `flexible_load_w` = plánované EV/TČ setpointy v daném slotu (pokud jsou připojené / povolené) -#### Doporučená implementace v EMS (aby se to chovalo správně) +#### Implementace v EMS (aktuální chování) -- **Správně (v solveru / plánu)**: řešit cut-off přímo v LP binární proměnnou `z_gen_cutoff[t]` (0/1), která modeluje, zda je GEN port odpojen. +- Cut-off se řeší přímo v LP binární proměnnou `z_gen_cutoff[t]` (0/1), která modeluje, zda je GEN port odpojen. - Efektivní výkon z GEN do bilance: `pv_b_effective[t] = pv_b_forecast_w * (1 - z_gen_cutoff[t])` - Solver nechá GEN připojený vždy, když je výkon užitečný (sníží import / nabije baterii / pokryje zátěž). - - Při `sell_price < 0` a zároveň hrozícím přebytku (ge je zakázané) solver může zvolit `z_gen_cutoff[t]=1` (cut-off) jako poslední možnost. + - `z_gen_cutoff[t]` je **vůbec povolené jen** v režimech/politikách, kde to dává smysl: + - `SELF_SUSTAIN` + - sloty s `sell_price < 0` („BLOCK_EXPORT“ okna) + - (případně) explicitní `no_export` politika, pokud je v kontextu dostupná + Mimo tyto případy je `z_gen_cutoff[t]` vynucené na `0`. + - Cut-off je v účelové funkci **penalizované** (za „zahozenou“ GEN výrobu), aby se zapínalo jen jako poslední možnost. - Výstup se ukládá do `planning_interval.deye_gen_cutoff_enabled` (nullable) a exporter pak jen provede reg 179. **Scope / bezpečnost:** proměnná i flag existují jen na lokalitách, kde je zapnutý `asset_inverter.deye_gen_microinverter_cutoff_enabled` (tj. kde je GEN port s mikroinvertory reálně zapojen). Jinde se nic neřeší ani nezobrazuje.