dalsi
This commit is contained in:
@@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0
|
|||||||
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
|
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
|
||||||
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
||||||
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
|
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
|
||||||
PLANNER_BUILD_TAG = "2026-05-29-night-selfconsume-evening-arb-v43"
|
PLANNER_BUILD_TAG = "2026-05-29-neg-day-pv-headroom-v44"
|
||||||
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
|
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
|
||||||
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0
|
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0
|
||||||
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
|
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
|
||||||
@@ -895,6 +895,20 @@ def _neg_sell_pv_b_charge_wh(slot: PlanningSlot, battery: Any) -> float:
|
|||||||
return cap_w * INTERVAL_H * float(battery.charge_efficiency)
|
return cap_w * INTERVAL_H * float(battery.charge_efficiency)
|
||||||
|
|
||||||
|
|
||||||
|
def _neg_sell_pv_forecast_charge_wh(slot: PlanningSlot, battery: Any) -> float:
|
||||||
|
"""Odhad Wh z FVE A+B v sell<0 slotu pro zpětnou projekci soc_need (v44)."""
|
||||||
|
pv_surplus = max(
|
||||||
|
0.0,
|
||||||
|
float(slot.pv_a_forecast_w)
|
||||||
|
+ float(slot.pv_b_forecast_w)
|
||||||
|
- float(slot.load_baseline_w),
|
||||||
|
)
|
||||||
|
if pv_surplus <= 500.0:
|
||||||
|
return 0.0
|
||||||
|
cap_w = min(pv_surplus, float(battery.max_charge_power_w))
|
||||||
|
return cap_w * INTERVAL_H * float(battery.charge_efficiency)
|
||||||
|
|
||||||
|
|
||||||
def _neg_sell_day_pv_b_usable_wh(
|
def _neg_sell_day_pv_b_usable_wh(
|
||||||
slots: list[PlanningSlot],
|
slots: list[PlanningSlot],
|
||||||
first_neg_sell_idx: int | None,
|
first_neg_sell_idx: int | None,
|
||||||
@@ -975,7 +989,9 @@ def _neg_sell_day_phases(
|
|||||||
indices.sort()
|
indices.sort()
|
||||||
last_t = indices[-1]
|
last_t = indices[-1]
|
||||||
tail_start = max(indices[0], last_t - tail_n + 1) if tail_n > 0 else last_t + 1
|
tail_start = max(indices[0], last_t - tail_n + 1) if tail_n > 0 else last_t + 1
|
||||||
charge_b = {t: _neg_sell_pv_b_charge_wh(slots[t], battery) for t in indices}
|
charge_b = {
|
||||||
|
t: _neg_sell_pv_forecast_charge_wh(slots[t], battery) for t in indices
|
||||||
|
}
|
||||||
soc_need: dict[int, float] = {last_t: soc_max}
|
soc_need: dict[int, float] = {last_t: soc_max}
|
||||||
for i in range(len(indices) - 1, 0, -1):
|
for i in range(len(indices) - 1, 0, -1):
|
||||||
t_cur = indices[i]
|
t_cur = indices[i]
|
||||||
@@ -3586,6 +3602,14 @@ def solve_dispatch(
|
|||||||
# v33: při dostatečné FVE v sell<0 okně neukládat ranní PV do baterie — export.
|
# v33: při dostatečné FVE v sell<0 okně neukládat ranní PV do baterie — export.
|
||||||
prob += bc_pv[t_pne] == 0
|
prob += bc_pv[t_pne] == 0
|
||||||
|
|
||||||
|
# v44: neg den — před 1. sell<0 žádné grid→bat (AM sloty za ~3 Kč vs FVE v okně).
|
||||||
|
if neg_sell_phases_en and first_neg_sell_idx is not None:
|
||||||
|
neg_day = _prague_calendar_date(slots[first_neg_sell_idx])
|
||||||
|
for t_blk in range(first_neg_sell_idx):
|
||||||
|
if _prague_calendar_date(slots[t_blk]) != neg_day:
|
||||||
|
continue
|
||||||
|
prob += bc_gi[t_blk] == 0
|
||||||
|
|
||||||
# Ekonomické guardy: ceny v objective nestačí proti maskám / terminal SoC.
|
# Ekonomické guardy: ceny v objective nestačí proti maskám / terminal SoC.
|
||||||
# Referenční buy jen z ne-záporných slotů: jinak jeden buy<0 v horizontu označí
|
# Referenční buy jen z ne-záporných slotů: jinak jeden buy<0 v horizontu označí
|
||||||
# téměř všechny sloty jako „drahé“ (gi=0 pro dům) → Infeasible (home-01).
|
# téměř všechny sloty jako „drahé“ (gi=0 pro dům) → Infeasible (home-01).
|
||||||
|
|||||||
@@ -4260,10 +4260,19 @@ class NegSellSocPhaseTests(unittest.TestCase):
|
|||||||
_ph, _tg, _w, meta = _neg_sell_day_phases(slots, bat)
|
_ph, _tg, _w, meta = _neg_sell_day_phases(slots, bat)
|
||||||
self.assertIsNotNone(meta.get("t_detach_idx"))
|
self.assertIsNotNone(meta.get("t_detach_idx"))
|
||||||
self.assertGreaterEqual(int(meta["t_detach_idx"]), 0)
|
self.assertGreaterEqual(int(meta["t_detach_idx"]), 0)
|
||||||
self.assertLess(int(meta["t_detach_idx"]), 8)
|
self.assertLessEqual(int(meta["t_detach_idx"]), 8)
|
||||||
self.assertGreater(float(meta.get("e_surplus_after_t_wh") or 0), 0.0)
|
self.assertGreater(float(meta.get("e_surplus_after_t_wh") or 0), 0.0)
|
||||||
self.assertIn("post_detach_prep_ts", meta)
|
self.assertIn("post_detach_prep_ts", meta)
|
||||||
|
|
||||||
|
def test_prep_leaves_headroom_when_pv_a_b_forecast_high(self) -> None:
|
||||||
|
"""v44: zpětná soc_need z A+B FVE, ne jen B — 1. sell<0 cíl pod soc_max."""
|
||||||
|
slots = self._neg_sell_slots(12, pv_a=8000, pv_b=6000)
|
||||||
|
bat = self._phase_battery(tail_slots=4)
|
||||||
|
_ph, targets, _w, meta = _neg_sell_day_phases(slots, bat)
|
||||||
|
first_neg = int(meta["days"][0]["first_neg_idx"])
|
||||||
|
tgt_first = float(targets[first_neg] or 0)
|
||||||
|
self.assertLess(tgt_first, bat.soc_max_wh * 0.95)
|
||||||
|
|
||||||
def test_t_detach_not_first_neg_on_long_sunny_day(self) -> None:
|
def test_t_detach_not_first_neg_on_long_sunny_day(self) -> None:
|
||||||
"""Bod T až po nabití rampy (~85 % soc_max), ne na prvním sell<0 slotu."""
|
"""Bod T až po nabití rampy (~85 % soc_max), ne na prvním sell<0 slotu."""
|
||||||
slots = self._neg_sell_slots(24, pv_b=7000, pv_a=5000)
|
slots = self._neg_sell_slots(24, pv_b=7000, pv_a=5000)
|
||||||
@@ -4703,6 +4712,82 @@ class NegSellPrepWindowV36Tests(unittest.TestCase):
|
|||||||
self.assertIsNotNone(snap["inputs"].get("neg_evening_export_budget_wh"))
|
self.assertIsNotNone(snap["inputs"].get("neg_evening_export_budget_wh"))
|
||||||
|
|
||||||
|
|
||||||
|
class NegDayPvHeadroomV44Tests(unittest.TestCase):
|
||||||
|
"""v44: neg den — žádný grid před sell<0; headroom pro FVE + levný buy v okně."""
|
||||||
|
|
||||||
|
def test_no_grid_charge_before_first_negative_sell(self) -> None:
|
||||||
|
prague = ZoneInfo("Europe/Prague")
|
||||||
|
base = datetime(2026, 5, 30, 5, 45, tzinfo=prague)
|
||||||
|
slots: list[PlanningSlot] = []
|
||||||
|
first_neg_idx: int | None = None
|
||||||
|
for i in range(24):
|
||||||
|
local = base + timedelta(minutes=15 * i)
|
||||||
|
sell = (
|
||||||
|
-0.18
|
||||||
|
if local.hour > 7 or (local.hour == 7 and local.minute >= 45)
|
||||||
|
else 3.0
|
||||||
|
)
|
||||||
|
if first_neg_idx is None and sell < 0:
|
||||||
|
first_neg_idx = i
|
||||||
|
buy = 3.2 if local.hour < 8 else 0.48
|
||||||
|
allow_chg = sell < 0
|
||||||
|
slots.append(
|
||||||
|
PlanningSlot(
|
||||||
|
interval_start=local.astimezone(timezone.utc),
|
||||||
|
buy_price=buy,
|
||||||
|
sell_price=sell,
|
||||||
|
pv_a_forecast_w=4000 if local.hour >= 8 else 500,
|
||||||
|
pv_b_forecast_w=3000 if local.hour >= 8 else 500,
|
||||||
|
load_baseline_w=2000,
|
||||||
|
ev1_connected=False,
|
||||||
|
ev2_connected=False,
|
||||||
|
allow_charge=allow_chg,
|
||||||
|
allow_discharge_export=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(first_neg_idx)
|
||||||
|
bat = NegSellSocPhaseTests._phase_battery()
|
||||||
|
hp = SimpleNamespace(
|
||||||
|
rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0
|
||||||
|
)
|
||||||
|
grid = SimpleNamespace(
|
||||||
|
max_import_power_w=17_000,
|
||||||
|
max_export_power_w=13_500,
|
||||||
|
block_export_on_negative_sell=False,
|
||||||
|
)
|
||||||
|
vehicles = [
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0
|
||||||
|
),
|
||||||
|
]
|
||||||
|
res, _, _ = solve_dispatch(
|
||||||
|
slots,
|
||||||
|
bat,
|
||||||
|
hp,
|
||||||
|
grid,
|
||||||
|
[None, None],
|
||||||
|
vehicles,
|
||||||
|
0.50 * bat.soc_max_wh,
|
||||||
|
50.0,
|
||||||
|
operating_mode="AUTO",
|
||||||
|
)
|
||||||
|
assert first_neg_idx is not None
|
||||||
|
for t in range(first_neg_idx):
|
||||||
|
self.assertLessEqual(
|
||||||
|
res[t].battery_setpoint_w,
|
||||||
|
200,
|
||||||
|
msg=f"grid/PV bat charge before neg at slot {t}",
|
||||||
|
)
|
||||||
|
self.assertLess(
|
||||||
|
res[first_neg_idx].battery_soc_target,
|
||||||
|
92.0,
|
||||||
|
"baterie nesmí být plná těsně před sell<0 oknem",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObservedSocNegPrepTests(unittest.TestCase):
|
class ObservedSocNegPrepTests(unittest.TestCase):
|
||||||
"""v40: neg-prep a večerní výboj z pozorovaného SoC (telemetrie), ne z LP trajektorie."""
|
"""v40: neg-prep a večerní výboj z pozorovaného SoC (telemetrie), ne z LP trajektorie."""
|
||||||
|
|
||||||
|
|||||||
@@ -874,7 +874,7 @@ begin
|
|||||||
end if;
|
end if;
|
||||||
end loop;
|
end loop;
|
||||||
|
|
||||||
-- v43: levný grid před prvním sell<0, když tentýž den večer (≥17h) dává arbitráž buy→sell.
|
-- v43: levný grid před prvním sell<0 jen na dnech BEZ sell<0 (normální arbitráž odpoledne→večer).
|
||||||
update _ems_plan_slot_wk wk
|
update _ems_plan_slot_wk wk
|
||||||
set allow_charge = true,
|
set allow_charge = true,
|
||||||
allow_grid_charge = true,
|
allow_grid_charge = true,
|
||||||
@@ -899,12 +899,25 @@ begin
|
|||||||
and wk.buy_price >= 0
|
and wk.buy_price >= 0
|
||||||
and wk.buy_price + v_degrad_czk_kwh < ep.evening_peak_sell
|
and wk.buy_price + v_degrad_czk_kwh < ep.evening_peak_sell
|
||||||
and extract(hour from wk.interval_start at time zone 'Europe/Prague')
|
and extract(hour from wk.interval_start at time zone 'Europe/Prague')
|
||||||
< v_evening_peak_start_hour
|
between 11 and 16
|
||||||
and (
|
and not exists (
|
||||||
v_first_neg_sell_ord is null
|
select 1
|
||||||
or wk.slot_ord < v_first_neg_sell_ord
|
from _ems_plan_slot_wk wn
|
||||||
|
where wn.sell_price < 0
|
||||||
|
and (wn.interval_start at time zone 'Europe/Prague')::date = ep.plan_date
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- v44: neg den — žádné grid nabíjení před 1. sell<0 (místo pro FVE; ne 3 Kč místo 0,5 Kč v okně).
|
||||||
|
if v_first_neg_sell_ord is not null and v_first_neg_prague_date is not null then
|
||||||
|
update _ems_plan_slot_wk wk
|
||||||
|
set allow_charge = false,
|
||||||
|
allow_grid_charge = false,
|
||||||
|
grid_charge_suppressed_reason = 'neg_day_no_grid_before_neg_sell'
|
||||||
|
where wk.slot_ord < v_first_neg_sell_ord
|
||||||
|
and (wk.interval_start at time zone 'Europe/Prague')::date = v_first_neg_prague_date
|
||||||
|
and wk.allow_grid_charge;
|
||||||
|
end if;
|
||||||
|
|
||||||
-- Ranní pásmo před prvním sell<0 (5–11 Prague): lokální peak, ne půlnoc celého dne.
|
-- Ranní pásmo před prvním sell<0 (5–11 Prague): lokální peak, ne půlnoc celého dne.
|
||||||
if v_first_neg_sell_ord is not null
|
if v_first_neg_sell_ord is not null
|
||||||
and v_first_neg_prague_date is not null
|
and v_first_neg_prague_date is not null
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]`** — výkon vybíjení na AC sběrnici z energetické bilance `pv + gi + bd = load + bc + ge`. Při exportu z baterie je v `bd` už započten i tok do sítě (`ge_bat` je součást `ge`); **`ge_bat` se v SoC znovu neodečítá** (dříve double-count → plán klesal ~2× rychleji než BMS ve večerním exportu). Tag `2026-05-28-evening-export-soc-balance-v39`.
|
- **SoC kontinuita a export z baterie:** `soc[t]` klesá při **`bd[t]`** — výkon vybíjení na AC sběrnici z energetické bilance `pv + gi + bd = load + bc + ge`. Při exportu z baterie je v `bd` už započten i tok do sítě (`ge_bat` je součást `ge`); **`ge_bat` se v SoC znovu neodečítá** (dříve double-count → plán klesal ~2× rychleji než BMS ve večerním exportu). Tag `2026-05-28-evening-export-soc-balance-v39`.
|
||||||
- **Masky `allow_charge` / `allow_discharge_export` (tenký anti-mikrocyklus):** generuje `ems.fn_load_planning_slots_full` (`R__063`). Ekonomiku primárně řídí LP podle efektivních cen; masky jen omezují počet slotů pro grid nabíjení / export baterie.
|
- **Masky `allow_charge` / `allow_discharge_export` (tenký anti-mikrocyklus):** generuje `ems.fn_load_planning_slots_full` (`R__063`). Ekonomiku primárně řídí LP podle efektivních cen; masky jen omezují počet slotů pro grid nabíjení / export baterie.
|
||||||
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity − sell − max(0, buy−sell)`; jen sloty s `sell ≥ buy − degradation`. Kumulativní PV pokrývá `grid_target` (deficit SoC, nad `reserve_soc` bez násobení `charge_slot_buffer`). Zbytek → `allow_charge=false` (PV jen do sítě / `bc ≤ pv_surplus` v LP).
|
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity − sell − max(0, buy−sell)`; jen sloty s `sell ≥ buy − degradation`. Kumulativní PV pokrývá `grid_target` (deficit SoC, nad `reserve_soc` bez násobení `charge_slot_buffer`). Zbytek → `allow_charge=false` (PV jen do sítě / `bc ≤ pv_surplus` v LP).
|
||||||
- **Grid ze sítě (vrstva B, před FVE):** výchozí **AM/PM 50/50** z `grid_target × charge_slot_buffer` (do `soc_max`); **nevyčerpaný AM Wh přejde do PM** (`R__063`). **Spot:** výběr **nejlevnější `buy`** (den plánu → před exportním oknem → `buy ASC`); navíc všechny sloty s **`buy < 0`** → `allow_grid_charge`. Po výběru AM/PM běží **iterativní self-konzistentní filtr** (vyloučí drahé grid sloty, pokud `pv_charge_wh_ahead + neg_buy_wh_ahead >= 60 %` deficitu SoC; failsafe unlock). **v43 `evening_arbitrage_unlock`:** před prvním sell<0 povolí grid nabíjení, když tentýž den večer (≥17h) `buy + degrad < evening_peak_sell`. Debug: `grid_charge_suppressed_reason`. **Fixní tarif (BA81):** stejný AM/PM rozpočet, ale pořadí podle **`slot_ord`** (buy konstantní), jen pokud v horizontu existuje **`sell > buy + degradation`**; jinak jen PV vrstva A. Cap slotů: `ceil(budget/per_slot_wh) × charge_slot_buffer`. **`charge_acquisition`:** vážený `buy` u `allow_grid_charge` před 1. exportem; two-pass v `planning_engine.py`.
|
- **Grid ze sítě (vrstva B, před FVE):** výchozí **AM/PM 50/50** z `grid_target × charge_slot_buffer` (do `soc_max`); **nevyčerpaný AM Wh přejde do PM** (`R__063`). **Spot:** výběr **nejlevnější `buy`** (den plánu → před exportním oknem → `buy ASC`); navíc všechny sloty s **`buy < 0`** → `allow_grid_charge`. Po výběru AM/PM běží **iterativní self-konzistentní filtr** (vyloučí drahé grid sloty, pokud `pv_charge_wh_ahead + neg_buy_wh_ahead >= 60 %` deficitu SoC; failsafe unlock). **v43 `evening_arbitrage_unlock`:** grid **11–16h** jen na dnech **bez sell<0**, když večer `buy + degrad < evening_peak_sell`. **v44 `neg_day_no_grid_before_neg_sell`:** na neg den **žádný grid před 1. sell<0**. Debug: `grid_charge_suppressed_reason`. **Fixní tarif (BA81):** stejný AM/PM rozpočet, ale pořadí podle **`slot_ord`** (buy konstantní), jen pokud v horizontu existuje **`sell > buy + degradation`**; jinak jen PV vrstva A. Cap slotů: `ceil(budget/per_slot_wh) × charge_slot_buffer`. **`charge_acquisition`:** vážený `buy` u `allow_grid_charge` před 1. exportem; two-pass v `planning_engine.py`.
|
||||||
- **PV vrstva A:** při `sell ≥ 0` jen pokud `sell ≥ future_sell_opportunity − degradation` (držet FVE na večerní peak). Při **`sell < 0`** vrstva A **bez** tohoto filtru (nabít z FVE v záporném výkupním okně). Historie: [`docs/planning-changelog.md`](../planning-changelog.md).
|
- **PV vrstva A:** při `sell ≥ 0` jen pokud `sell ≥ future_sell_opportunity − degradation` (držet FVE na večerní peak). Při **`sell < 0`** vrstva A **bez** tohoto filtru (nabít z FVE v záporném výkupním okně). Historie: [`docs/planning-changelog.md`](../planning-changelog.md).
|
||||||
- **LP (AUTO):** objective explicitně `−ge_pv×sell − ge_bat×sell + ge_bat×acquisition` v exportních slotech; **bez** cross-slot vynucení `ge_pv ≥ surplus`. Guard FVE: `ge_pv=0` jen pokud `sell < charge_acquisition − degrad` (ne `sell < buy` ve slotu). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md).
|
- **LP (AUTO):** objective explicitně `−ge_pv×sell − ge_bat×sell + ge_bat×acquisition` v exportních slotech; **bez** cross-slot vynucení `ge_pv ≥ surplus`. Guard FVE: `ge_pv=0` jen pokud `sell < charge_acquisition − degrad` (ne `sell < buy` ve slotu). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md).
|
||||||
- **Load-first (Deye, AUTO, tvrdý od v34):** proměnné `pv_ld` (PV → load+EV+TČ), `pv_sp` (přebytek), `bc_pv` / `bc_gi`. Plná bilance `pv_a + pv_b + gi + bd = load + ev + hp + bc + ge`; `bc_pv + ge_pv ≤ pv_sp`; **`gi ≤ bc_gi + max(0, max_load − pv_forecast)`** (při vysoké FVE žádný fiktivní import = load); při `pv ≥ load + 500 W` **`pv_ld ≥ load`**; mimo `allow_discharge_export`: `bd ≤ load − pv_ld`, `pv_ld ≥ load − bd`. Tag `2026-05-28-load-first-hard-v34`. Test `LoadFirstDispatchTests`.
|
- **Load-first (Deye, AUTO, tvrdý od v34):** proměnné `pv_ld` (PV → load+EV+TČ), `pv_sp` (přebytek), `bc_pv` / `bc_gi`. Plná bilance `pv_a + pv_b + gi + bd = load + ev + hp + bc + ge`; `bc_pv + ge_pv ≤ pv_sp`; **`gi ≤ bc_gi + max(0, max_load − pv_forecast)`** (při vysoké FVE žádný fiktivní import = load); při `pv ≥ load + 500 W` **`pv_ld ≥ load`**; mimo `allow_discharge_export`: `bd ≤ load − pv_ld`, `pv_ld ≥ load − bd`. Tag `2026-05-28-load-first-hard-v34`. Test `LoadFirstDispatchTests`.
|
||||||
@@ -100,10 +100,15 @@ flowchart TD
|
|||||||
3. **v43 — večerní push + nocí vlastní spotřeba + odpolední arbitráž** (`evening_push_ts`):
|
3. **v43 — večerní push + nocí vlastní spotřeba + odpolední arbitráž** (`evening_push_ts`):
|
||||||
- push jen **≥17h Prague** + `allow_discharge_export`; rozpočet Wh **per kalendářní večer** (druhý den v horizontu ne prázdný);
|
- push jen **≥17h Prague** + `allow_discharge_export`; rozpočet Wh **per kalendářní večer** (druhý den v horizontu ne prázdný);
|
||||||
- mimo push: **`night_self_consume_discourage`** — baterie krmí dům, ne import ~5 Kč/kWh;
|
- mimo push: **`night_self_consume_discourage`** — baterie krmí dům, ne import ~5 Kč/kWh;
|
||||||
- **R__063 `evening_arbitrage_unlock`:** grid nabíjení odpoledne před sell<0, když večerní peak sell > buy + degrad;
|
- **R__063 `evening_arbitrage_unlock`:** grid nabíjení **11–16h** jen na dnech **bez sell<0**, když večerní peak sell > buy + degrad;
|
||||||
- **bez predawn push** (02–06h); **`peak_export_shortfall`** v noci vypnutý.
|
- **bez predawn push** (02–06h); **`peak_export_shortfall`** v noci vypnutý.
|
||||||
|
|
||||||
**Funkce:** `_evening_push_calendar_segments`, `_night_self_consume_discourage_import_indices`, `_in_evening_push_hour_window`, … Tag: **`2026-05-29-night-selfconsume-evening-arb-v43`**.
|
4. **v44 — neg den: místo pro FVE před sell<0 oknem:**
|
||||||
|
- **`neg_day_no_grid_before_neg_sell`:** na kalendářní den s sell<0 **žádné grid nabíjení před 1. sell<0** (ne 3 Kč ráno místo 0,5 Kč v okně);
|
||||||
|
- **`_neg_sell_pv_forecast_charge_wh`:** zpětná soc_need z **A+B** FVE, ne jen pole B;
|
||||||
|
- LP **`bc_gi=0`** před 1. sell<0 na neg den.
|
||||||
|
|
||||||
|
**Funkce:** `_evening_push_calendar_segments`, `_night_self_consume_discourage_import_indices`, `_neg_sell_pv_forecast_charge_wh`, … Tag: **`2026-05-29-neg-day-pv-headroom-v44`**.
|
||||||
|
|
||||||
### Arbitráž baterie — účtování mezi sloty (povinné čtení)
|
### Arbitráž baterie — účtování mezi sloty (povinné čtení)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,22 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-29 — Neg den: headroom pro FVE, ne grid za 3 Kč před sell<0 (v44)
|
||||||
|
|
||||||
|
**Problém (v43 na home-01 30. 5.):** Ráno **05:45–07:30** grid+bat nabíjení za **~2,6–3,7 Kč/kWh** → SoC **~99 %** ještě před **07:45 sell<0**. Pak **PV A plně utlumena**, **PV B** do site za záporný sell; levný **buy ~0,48 Kč** v 11h nevyužit. Příčiny: (1) **`evening_arbitrage_unlock`** povolil drahý grid před neg oknem; (2) AM maska brala nejlevnější buy **před polednem**, ne v neg okně; (3) **`soc_need`** zpětně počítal jen **PV B**, ne A+B → cíl prep ≈ **soc_max**.
|
||||||
|
|
||||||
|
**Změna (v44):**
|
||||||
|
- **`evening_arbitrage_unlock`** jen na dnech **bez sell<0**, hodiny **11–16** (normální odpolední→večerní arbitráž).
|
||||||
|
- **`neg_day_no_grid_before_neg_sell`:** na neg kalendářní den **`allow_grid_charge=false`** pro všechny sloty **před 1. sell<0**.
|
||||||
|
- **`_neg_sell_pv_forecast_charge_wh`:** zpětná projekce soc_need z **FVE A+B** surplusu, ne jen B.
|
||||||
|
- **LP:** `bc_gi[t]=0` před 1. sell<0 na neg den (pás pro případ masky).
|
||||||
|
|
||||||
|
**Soubory:** `planning_engine.py`, `R__063_fn_load_planning_slots_full.sql`, `test_planning_dispatch_milp.py`, `planning.md`. Tag **`2026-05-29-neg-day-pv-headroom-v44`**.
|
||||||
|
|
||||||
|
**Ověření:** `pytest … -k "NegDayPvHeadroom or prep_leaves_headroom"`; MCP: před 07:45 `allow_grid_charge=false`, `grid_charge_suppressed_reason=neg_day_no_grid_before_neg_sell`; SoC před neg < ~90 %; po svítání PV A ne plný curtail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-05-29 — Noc: vlastní spotřeba + večerní arbitráž + push per den (v43)
|
## 2026-05-29 — Noc: vlastní spotřeba + večerní arbitráž + push per den (v43)
|
||||||
|
|
||||||
**Problém:** (1) Po v42 push exportu plán přes noc **držel SoC ~60 %** a krmil dům ze sítě za **~5 Kč/kWh** místo baterie (acq ~0,7 Kč). (2) Tvrdý push zahrnoval **02–06h** (sell < buy). (3) **Druhý večer** v horizontu neměl push — rozpočet Wh se vyčerpal první nocí. (4) Před neg dnem **grid 0,5 Kč** odpoledne nešel nabíjet (`allow_charge=false`, cheaper_pv_ahead), přitom večer sell **~4 Kč** — arbitráž neproběhla.
|
**Problém:** (1) Po v42 push exportu plán přes noc **držel SoC ~60 %** a krmil dům ze sítě za **~5 Kč/kWh** místo baterie (acq ~0,7 Kč). (2) Tvrdý push zahrnoval **02–06h** (sell < buy). (3) **Druhý večer** v horizontu neměl push — rozpočet Wh se vyčerpal první nocí. (4) Před neg dnem **grid 0,5 Kč** odpoledne nešel nabíjet (`allow_charge=false`, cheaper_pv_ahead), přitom večer sell **~4 Kč** — arbitráž neproběhla.
|
||||||
|
|||||||
Reference in New Issue
Block a user