uprava solver gneport cutoff u ba81
Some checks failed
CI and deploy / migration-check (push) Failing after 25s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-26 19:55:35 +02:00
parent bf7373fbfe
commit 4875c31338
4 changed files with 42 additions and 10 deletions

View File

@@ -2,6 +2,7 @@
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$PROJECT_DIR$/../ems-cursor-db-pracovni/debug-forecast.sql" value="26ebef46-ff23-475b-8adc-082723263a02" />
<file url="file://$PROJECT_DIR$/../ems-cursor-db-pracovni/naplneni-base-line-ba81.sql" value="26ebef46-ff23-475b-8adc-082723263a02" />
<file url="file://$PROJECT_DIR$/../ems-cursor-db-pracovni/porovnani-view-status.sql" value="26ebef46-ff23-475b-8adc-082723263a02" />
<file url="file://$PROJECT_DIR$/db/migration/V009__postgrest_roles.sql" value="26ebef46-ff23-475b-8adc-082723263a02" />
<file url="file://$PROJECT_DIR$/db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql" value="26ebef46-ff23-475b-8adc-082723263a02" />

View File

@@ -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):

View File

@@ -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 | — |

View File

@@ -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.