revert a nove upravy
This commit is contained in:
@@ -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-27-neg-sell-soc-reservation-v13"
|
||||
PLANNER_BUILD_TAG = "2026-05-27-simple-buy-neg-window-v16"
|
||||
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
|
||||
@@ -1200,6 +1200,28 @@ def solve_dispatch(
|
||||
discharge_export_slots = {
|
||||
t for t, s in enumerate(slots) if s.allow_discharge_export
|
||||
}
|
||||
# Vybití baterie před `buy<0` oknem: pokud je v horizontu buy<0, můžeme baterii
|
||||
# vybít teď za `sell` a v buy<0 okně ji nabít za záporný buy (= příjem).
|
||||
# Ekonomicky výhodné dokud: sell_t > avg_buy_neg + degradation
|
||||
# (vybíjet ztratíme ~discharge_eff loss, nabíjení v buy<0 nás platí; marže ~ sell − buy_neg − degrad).
|
||||
# Cílem je obejít to, že R__063 v noci dává allow_discharge_export=false a LP
|
||||
# by jinak nemohl baterku vyklidit přes ge_bat.
|
||||
_neg_buy_for_disch = next(
|
||||
(t for t, s in enumerate(slots) if float(s.buy_price) < 0), None
|
||||
)
|
||||
if _neg_buy_for_disch is not None and _neg_buy_for_disch > 0:
|
||||
_neg_buy_prices = [
|
||||
float(slots[t].buy_price)
|
||||
for t in range(_neg_buy_for_disch, T)
|
||||
if float(slots[t].buy_price) < 0
|
||||
]
|
||||
_avg_neg_buy = sum(_neg_buy_prices) / len(_neg_buy_prices) if _neg_buy_prices else 0.0
|
||||
# Práh = avg buy<0 + degradation cycle overhead; default fallback 0.1 Kč/kWh
|
||||
# když z nějakého důvodu neumíme spočítat (ochrana proti vybití do mínusu).
|
||||
_disch_sell_thr = max(_avg_neg_buy + float(degradation_cost_effective), 0.1)
|
||||
for t in range(_neg_buy_for_disch):
|
||||
if float(slots[t].sell_price) >= _disch_sell_thr:
|
||||
discharge_export_slots.add(t)
|
||||
# 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,
|
||||
@@ -1913,6 +1935,16 @@ def solve_dispatch(
|
||||
prob += bd[t] == 0
|
||||
|
||||
# Slot pre-selection (z DB fn_load_planning_slots_full → allow_*)
|
||||
# PŘED prvním buy<0 slotem v horizontu (= rezervační okno):
|
||||
# - sell ≥ 0 → bc_pv = 0 (PV poteče do gridu / curtail, baterka se nenabíjí z PV
|
||||
# protože v buy<0 okně bude akvizice levnější — záporná).
|
||||
# - sell < 0 → slot je v charge_slots (R__063), bc_pv ≤ pv_surplus (= nemůžeme
|
||||
# pole A vyhodit do mínusu, raději nabít baterii).
|
||||
# JINDY (po buy<0 okně, nebo žádné buy<0 v horizontu): původní permissive
|
||||
# bc_pv ≤ pv_surplus aby nedošlo k regresi normálních dnů.
|
||||
_neg_buy_idx_main = next(
|
||||
(t for t, s in enumerate(slots) if float(s.buy_price) < 0), None
|
||||
)
|
||||
if om == "AUTO":
|
||||
for t in range(T):
|
||||
if t not in charge_slots:
|
||||
@@ -1923,11 +1955,17 @@ def solve_dispatch(
|
||||
+ int(s.pv_b_forecast_w)
|
||||
- int(s.load_baseline_w),
|
||||
)
|
||||
# Mimo grid-charge masku: jen PV přebytek; výjimka záporný buy (spot arbitráž).
|
||||
if float(s.buy_price) >= 0.0:
|
||||
prob += bc_gi[t] == 0
|
||||
in_pre_neg_buy_window = (
|
||||
_neg_buy_idx_main is not None and t < _neg_buy_idx_main
|
||||
)
|
||||
if pv_surplus_w <= 0:
|
||||
prob += bc_pv[t] == 0
|
||||
elif in_pre_neg_buy_window:
|
||||
# Strukturální preference: PV jde do gridu (sell≥0) nebo curtail,
|
||||
# ne do baterie — kapacitu si šetříme na buy<0 okno.
|
||||
prob += bc_pv[t] == 0
|
||||
else:
|
||||
prob += bc_pv[t] <= float(pv_surplus_w)
|
||||
if t not in discharge_export_slots and t not in neg_sell_bat_dump_slots:
|
||||
|
||||
@@ -1230,7 +1230,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-simple-buy-neg-window-v16")
|
||||
self.assertGreater(
|
||||
results[0].battery_setpoint_w,
|
||||
5_500,
|
||||
@@ -1380,7 +1380,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-simple-buy-neg-window-v16")
|
||||
self.assertEqual(len(results), len(slots))
|
||||
|
||||
def test_gen_cutoff_full_soc_neg_sell_with_pv_b_feasible(self) -> None:
|
||||
@@ -1444,7 +1444,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
55.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-neg-sell-soc-reservation-v13")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-27-simple-buy-neg-window-v16")
|
||||
self.assertEqual(len(results), len(slots))
|
||||
|
||||
def test_fixed_tariff_neg_sell_no_grid_export(self) -> None:
|
||||
|
||||
@@ -5,6 +5,31 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-27 (f) — zjednodušená strategie pro buy<0 okno (v16, revert v14+v15)
|
||||
|
||||
**Problém v14/v15 (run 16622, 16636, 16642):** Vrstvy soft penalty (cap+slack, PV charge suppressed penalty) LP **nedonutily** vybít baterii ani omezit PV pumping. LP přijímal sloupec slack 24 kWh × 50 Kč/kWh = 1190 Kč a baterii nabíjel z ranního PV (10:30 SoC=95 %), pak v `buy<0` okně (13:00–14:45) curtail pole A 5–9 kW + export pole A do mínusu.
|
||||
|
||||
**Strukturální root cause (3 vrstvy):**
|
||||
1. R__063 `allow_charge=false` ze SQL Pythonský `solve_dispatch` ignoruje pro PV charging (`bc_pv ≤ pv_surplus` i pro `t not in charge_slots`).
|
||||
2. `discharge_export_slots` v noci `false` (R__063) → LP nemá cestu jak baterii vybít přes ge_bat.
|
||||
3. `acquisition` v LP je vstupní konstanta — LP nevidí, že buy<0 okno je „lepší cesta" než ranní PV pumping.
|
||||
|
||||
**Oprava (tag `2026-05-27-simple-buy-neg-window-v16`):** Reverted v14+v15, znovu postaveno **2 jednoduchá pravidla** podle business logiky:
|
||||
|
||||
1. **Tvrdé `bc_pv[t] = 0` pre-first_neg_buy_idx** (slots kde `t not in charge_slots`): PV poteče do gridu (sell≥0) nebo curtail, ne do baterie. R__063 už pro `sell<0+pv_surplus` přidává `allow_charge=true` (= `t in charge_slots`), takže pole A v `sell<0` slotech může nabíjet baterii (= nevyhodit do mínusu).
|
||||
2. **Rozšíření `discharge_export_slots`** o pre-`buy<0` sloty se **dynamickým prahem** `sell ≥ max(avg(buy<0) + degradation_cost, 0.1) Kč/kWh`. Pro home-01 (avg buy<0 ≈ −0,22, degrad ≈ 0,15) to dělá práh ~0,1 Kč → prakticky všechny noční sloty se `sell > 0`. Ekonomická logika: marže `sell_t − acquisition_in_neg_buy_window − degradation`, a pokud `acquisition ≈ záporný` (buy<0 v okně), je výhodné vybít a znovu nabít i za sell ~1 Kč/kWh.
|
||||
|
||||
**Business logika (od uživatele):**
|
||||
- Noc před `buy<0`: vybít baterii za sell ~3 Kč/kWh.
|
||||
- Ráno: minimální SoC.
|
||||
- `buy<0` okno: PV B necurtailovat (R__063 už řeší), nabíjet ze sítě (LP samo, buy záporný = `t in charge_slots`).
|
||||
- Po `sell>0`: baterie plná, max prodej.
|
||||
- Večer: prodat zbytek.
|
||||
|
||||
**Ověření:** `pytest backend/tests/test_planning_dispatch_milp.py tests/test_planning_charge_slot_selection.py` — 87 passed (1 pre-existing fail nesouvisí). Po deploy MCP: `select pr.solver_params->'planner_build_tag'` = `…-v16`, plán home-01 25.5.: SoC v 12:45 < 50 %, 13:00–14:45 SoC roste z capu k ~95 %, `pv_a_curtailed_w` blízko 0 v okně.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-27 (c) — rezervace SoC pro `sell<0` okno + fallback acquisition ≥ 0 (v13)
|
||||
|
||||
**Problém (home-01 run 16614, tag v12):** Aktivní plán pro 2026-05-25:
|
||||
|
||||
Reference in New Issue
Block a user