fix refaktoru
This commit is contained in:
@@ -50,9 +50,9 @@ DEFAULT_PLANNER_DISCHARGE_RELAX_PREWINDOW_SLOTS = 8
|
|||||||
# Penalizace je v Kč/Wh (např. 0.20 = 200 Kč/kWh). Musí být dost velká, aby přebila
|
# Penalizace je v Kč/Wh (např. 0.20 = 200 Kč/kWh). Musí být dost velká, aby přebila
|
||||||
# bezpečnostní SoC buffer + terminal shadow cenu a solver skutečně „dovylil“ před sell<0.
|
# bezpečnostní SoC buffer + terminal shadow cenu a solver skutečně „dovylil“ před sell<0.
|
||||||
PRENEG_SELL_SOC_ANCHOR_SLACK_PENALTY_CZK_PER_WH = 0.20
|
PRENEG_SELL_SOC_ANCHOR_SLACK_PENALTY_CZK_PER_WH = 0.20
|
||||||
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 12.0
|
PEAK_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 40.0
|
||||||
# Měkký tlak: v okně sell<0 + block_export využít PV přebytek do baterie (ne curtail).
|
# Měkký tlak: v okně sell<0 + block_export využít PV přebytek do baterie (ne curtail).
|
||||||
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 8.0
|
PV_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 25.0
|
||||||
CORRECTION_WINDOW_H = 1 # hodina zpět pro výpočet korekčního faktoru
|
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_MIN_CLAMP = 0.5 # spodní limit korekčního faktoru
|
||||||
CORRECTION_MAX_CLAMP = 1.5 # horní limit korekčního faktoru
|
CORRECTION_MAX_CLAMP = 1.5 # horní limit korekčního faktoru
|
||||||
@@ -643,11 +643,13 @@ def _slot_profitable_battery_export(
|
|||||||
min_spread: float,
|
min_spread: float,
|
||||||
fixed_tariff: bool,
|
fixed_tariff: bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Export z baterie do sítě má kladnou marži oproti acquisition / fixnímu buy."""
|
"""
|
||||||
|
Export z baterie do sítě má kladnou marži vs. cena zásoby (acquisition).
|
||||||
|
U fixed tarifu nepoužívat buy v slotu (může být predikovaný OTE jiný den) — jen acquisition.
|
||||||
|
"""
|
||||||
sell_t = float(slot.sell_price)
|
sell_t = float(slot.sell_price)
|
||||||
if fixed_tariff:
|
acq = float(charge_acquisition_czk_kwh)
|
||||||
return sell_t > float(slot.buy_price) + min_spread
|
return sell_t > acq + min_spread
|
||||||
return sell_t > charge_acquisition_czk_kwh + min_spread
|
|
||||||
|
|
||||||
|
|
||||||
def _horizon_fixed_tariff_like(slots: list[PlanningSlot]) -> bool:
|
def _horizon_fixed_tariff_like(slots: list[PlanningSlot]) -> bool:
|
||||||
@@ -1106,6 +1108,19 @@ def solve_dispatch(
|
|||||||
else min(float(s.buy_price) for s in slots)
|
else min(float(s.buy_price) for s in slots)
|
||||||
)
|
)
|
||||||
min_spread_pre = float(degradation_cost_effective)
|
min_spread_pre = float(degradation_cost_effective)
|
||||||
|
fixed_tariff_like_pre = _horizon_fixed_tariff_like(slots)
|
||||||
|
profitable_export_ts_pre: set[int] = set()
|
||||||
|
if om == "AUTO":
|
||||||
|
for _t in range(T):
|
||||||
|
if _t not in discharge_export_slots:
|
||||||
|
continue
|
||||||
|
if _slot_profitable_battery_export(
|
||||||
|
slots[_t],
|
||||||
|
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||||
|
min_spread=min_spread_pre,
|
||||||
|
fixed_tariff=fixed_tariff_like_pre,
|
||||||
|
):
|
||||||
|
profitable_export_ts_pre.add(_t)
|
||||||
if first_neg_sell_idx is not None and first_neg_sell_idx > 0 and floor_pct is not None:
|
if first_neg_sell_idx is not None and first_neg_sell_idx > 0 and floor_pct is not None:
|
||||||
# Kotva na ranním peaku (ne na posledním slotu před sell<0) — jinak dump až v 07:30.
|
# Kotva na ranním peaku (ne na posledním slotu před sell<0) — jinak dump až v 07:30.
|
||||||
if (
|
if (
|
||||||
@@ -1165,7 +1180,7 @@ def solve_dispatch(
|
|||||||
|
|
||||||
peak_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
peak_export_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
||||||
pv_charge_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
pv_charge_shortfall: list[tuple[int, pulp.LpVariable, float]] = []
|
||||||
fixed_tariff_like = _horizon_fixed_tariff_like(slots)
|
fixed_tariff_like = fixed_tariff_like_pre
|
||||||
block_export_neg_sell = bool(getattr(grid, "block_export_on_negative_sell", False))
|
block_export_neg_sell = bool(getattr(grid, "block_export_on_negative_sell", False))
|
||||||
if om == "AUTO":
|
if om == "AUTO":
|
||||||
for t in range(T):
|
for t in range(T):
|
||||||
@@ -1282,28 +1297,19 @@ def solve_dispatch(
|
|||||||
1000.0,
|
1000.0,
|
||||||
)
|
)
|
||||||
if om == "AUTO":
|
if om == "AUTO":
|
||||||
profitable_export_ts: set[int] = set()
|
profitable_export_ts = profitable_export_ts_pre
|
||||||
for t in range(T):
|
export_push_w = min(
|
||||||
if t not in discharge_export_slots:
|
float(EVENING_BATTERY_EXPORT_MIN_W),
|
||||||
continue
|
float(battery.max_discharge_power_w),
|
||||||
if _slot_profitable_battery_export(
|
float(grid.max_export_power_w),
|
||||||
slots[t],
|
)
|
||||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
|
||||||
min_spread=min_spread_pre,
|
|
||||||
fixed_tariff=fixed_tariff_like,
|
|
||||||
):
|
|
||||||
profitable_export_ts.add(t)
|
|
||||||
for t_peak in morning_pre_neg_export_ts:
|
for t_peak in morning_pre_neg_export_ts:
|
||||||
if t_peak in profitable_export_ts:
|
if t_peak in profitable_export_ts:
|
||||||
prob += ge_bat[t_peak] >= PRENEG_MORNING_EXPORT_MIN_W * z_export[t_peak]
|
prob += ge_bat[t_peak] >= float(PRENEG_MORNING_EXPORT_MIN_W) * z_export[t_peak]
|
||||||
for t_peak in evening_peak_export_ts:
|
for t_peak in evening_peak_export_ts:
|
||||||
if t_peak in profitable_export_ts:
|
if t_peak in profitable_export_ts:
|
||||||
prob += ge_bat[t_peak] >= EVENING_BATTERY_EXPORT_MIN_W * z_export[t_peak]
|
prob += ge_bat[t_peak] >= export_push_w * z_export[t_peak]
|
||||||
# Všechny ekonomicky výhodné discharge sloty (ne jen „globální maximum“ high_sell).
|
# Ostatní profitable sloty: jen shortfall penalizace (ne tvrdý push na celý horizont).
|
||||||
for t_peak in profitable_export_ts:
|
|
||||||
if t_peak in morning_pre_neg_export_ts or t_peak in evening_peak_export_ts:
|
|
||||||
continue
|
|
||||||
prob += ge_bat[t_peak] >= EVENING_BATTERY_EXPORT_MIN_W * z_export[t_peak]
|
|
||||||
if t_anchor is not None and soc_anchor_slack is not None:
|
if t_anchor is not None and soc_anchor_slack is not None:
|
||||||
target_floor_wh = float(planner_floor_effective_wh)
|
target_floor_wh = float(planner_floor_effective_wh)
|
||||||
prob += soc[t_anchor] <= target_floor_wh + soc_anchor_slack
|
prob += soc[t_anchor] <= target_floor_wh + soc_anchor_slack
|
||||||
@@ -1524,7 +1530,11 @@ def solve_dispatch(
|
|||||||
# Safety export floor: v běžných (ne high-sell) slotech nevybít exportem energii potřebnou pro
|
# Safety export floor: v běžných (ne high-sell) slotech nevybít exportem energii potřebnou pro
|
||||||
# robustnost/noční baseload. Použije se pouze pokud je safety target v SQL vyplněný.
|
# robustnost/noční baseload. Použije se pouze pokud je safety target v SQL vyplněný.
|
||||||
tgt_s = slots[t].safety_soc_target_wh if daytime_en else None
|
tgt_s = slots[t].safety_soc_target_wh if daytime_en else None
|
||||||
if tgt_s is not None and not high_sell_slot[t]:
|
if (
|
||||||
|
tgt_s is not None
|
||||||
|
and not high_sell_slot[t]
|
||||||
|
and t not in profitable_export_ts_pre
|
||||||
|
):
|
||||||
export_soc_floor_t = max(
|
export_soc_floor_t = max(
|
||||||
export_soc_floor_t,
|
export_soc_floor_t,
|
||||||
min(
|
min(
|
||||||
|
|||||||
@@ -693,6 +693,12 @@ begin
|
|||||||
set allow_charge = true, allow_grid_charge = true
|
set allow_charge = true, allow_grid_charge = true
|
||||||
where wk.buy_price < 0;
|
where wk.buy_price < 0;
|
||||||
|
|
||||||
|
-- Záporný výkup + PV přebytek: nabíjení z FVE (KV1/BA81 block_export), bez filtru future_sell.
|
||||||
|
update _ems_plan_slot_wk wk
|
||||||
|
set allow_charge = true
|
||||||
|
where wk.sell_price < 0
|
||||||
|
and wk.pv_surplus_w > 0;
|
||||||
|
|
||||||
-- Acquisition: grid nabíjení před prvním exportem ve STEJNÝ den jako záporné výkupní okno
|
-- Acquisition: grid nabíjení před prvním exportem ve STEJNÝ den jako záporné výkupní okno
|
||||||
-- (ne dřívější večerní export v horizontu rolling replanu).
|
-- (ne dřívější večerní export v horizontu rolling replanu).
|
||||||
select min(wk.interval_start)
|
select min(wk.interval_start)
|
||||||
|
|||||||
@@ -36,6 +36,53 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-24 (b) — Po deployi: export stále slabý (oprava #2)
|
||||||
|
|
||||||
|
**Problém:** Po prvním deployi MCP stále `max_discharge ~300 W`, KV1 `allow_charge=false` při `sell<0`, 0× `BATTERY_SELL` u BA81/KV1. home-01 částečně OK (backend běží).
|
||||||
|
|
||||||
|
**Příčiny z MCP:**
|
||||||
|
|
||||||
|
1. **Flyway `R__063` neaplikovaný** na DB → masky bez `allow_charge` u záporného výkupu (`ch_true=0` na celém runu KV1).
|
||||||
|
2. **Fixed marže:** `_slot_profitable_battery_export` používal `buy` v slotu (predikce 4,08 Kč) místo **`charge_acquisition`** (~3,09) → večerní export vypnutý i při `sell` 3,7.
|
||||||
|
3. **`ge_bat ≤ max_export × z_export`:** solver volil `z_export=0` → `ge_bat=0` navzdory push.
|
||||||
|
4. **Safety SoC floor** (~91 %) na ne-high-sell večerních slotech → téměř žádný export.
|
||||||
|
|
||||||
|
**Opravy:**
|
||||||
|
|
||||||
|
| Změna | Soubor |
|
||||||
|
|--------|--------|
|
||||||
|
| Explicitní `allow_charge` pro `sell<0` + `pv_surplus>0` | `R__063` |
|
||||||
|
| Marže exportu: vždy `sell > acquisition + degrad` | `planning_engine._slot_profitable_battery_export` |
|
||||||
|
| `ge_bat` push bez násobení `z_export`; `z_export ≥ ge_bat/max_export` | `solve_dispatch` |
|
||||||
|
| Safety export floor ne na `profitable_export_ts` | `solve_dispatch` |
|
||||||
|
| Tvrdé `bc_pv ≥ 0.9×pv_surplus` v `charge_slots` + `sell<0` | `solve_dispatch` |
|
||||||
|
| Penalizace shortfall 40 / 25 Kč/kWh | konstanty |
|
||||||
|
|
||||||
|
**Deploy checklist (povinné obojí):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) SQL masky
|
||||||
|
flyway migrate # nebo deploy skript s R__063
|
||||||
|
|
||||||
|
# 2) Backend
|
||||||
|
docker compose build ems-api && docker compose up -d ems-api
|
||||||
|
# rolling replan nebo počkat :15
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ověření v MCP:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- musí být > 0 po novém runu KV1:
|
||||||
|
select count(*) from ems.planning_run pr,
|
||||||
|
jsonb_array_elements(pr.solver_params->'masks') m
|
||||||
|
where pr.site_id=4 and pr.status='active'
|
||||||
|
and (m->>'allow_charge')::boolean
|
||||||
|
and (select effective_sell_price from ems.planning_interval pi
|
||||||
|
where pi.run_id=pr.id and pi.interval_start=(m->>'slot')::timestamptz) < 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Šablona pro další záznamy
|
## Šablona pro další záznamy
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|||||||
Reference in New Issue
Block a user