dalsi a dalsi oprava
This commit is contained in:
@@ -31,18 +31,26 @@ def _prague_date(s: PlanningSlot) -> date:
|
||||
return s.interval_start.astimezone(_PRAGUE).date()
|
||||
|
||||
|
||||
def _export_window_start(slots: list[PlanningSlot], degrad: float) -> datetime | None:
|
||||
"""Kopie R__063: sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
|
||||
result: datetime | None = None
|
||||
def _export_window_start_by_day(
|
||||
slots: list[PlanningSlot], degrad: float
|
||||
) -> dict[date, datetime]:
|
||||
"""Kopie R__063: první sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
|
||||
out: dict[date, datetime] = {}
|
||||
for s in slots:
|
||||
day = _prague_date(s)
|
||||
day_min = min(
|
||||
float(x.buy_price) for x in slots if _prague_date(x) == day
|
||||
)
|
||||
day_min = min(float(x.buy_price) for x in slots if _prague_date(x) == day)
|
||||
if float(s.sell_price) > day_min + degrad:
|
||||
if result is None or s.interval_start < result:
|
||||
result = s.interval_start
|
||||
return result
|
||||
prev = out.get(day)
|
||||
if prev is None or s.interval_start < prev:
|
||||
out[day] = s.interval_start
|
||||
return out
|
||||
|
||||
|
||||
def _before_day_export(
|
||||
slots: list[PlanningSlot], t: int, export_by_day: dict[date, datetime]
|
||||
) -> bool:
|
||||
start = export_by_day.get(_prague_date(slots[t]))
|
||||
return start is not None and slots[t].interval_start < start
|
||||
|
||||
|
||||
def _future_sell(slots: list[PlanningSlot], t: int) -> float:
|
||||
@@ -55,13 +63,13 @@ def _buy_min_next_n(
|
||||
t: int,
|
||||
n: int = _LOOKAHEAD_SLOTS,
|
||||
*,
|
||||
export_window_start: datetime | None = None,
|
||||
export_by_day: dict[date, datetime] | None = None,
|
||||
) -> float | None:
|
||||
tail = [
|
||||
float(slots[i].buy_price)
|
||||
for i in range(t + 1, min(t + 1 + n, len(slots)))
|
||||
if export_window_start is None or slots[i].interval_start < export_window_start
|
||||
]
|
||||
tail: list[float] = []
|
||||
for i in range(t + 1, min(t + 1 + n, len(slots))):
|
||||
day_start = export_by_day.get(_prague_date(slots[i])) if export_by_day else None
|
||||
if day_start is None or slots[i].interval_start < day_start:
|
||||
tail.append(float(slots[i].buy_price))
|
||||
return min(tail) if tail else None
|
||||
|
||||
|
||||
@@ -100,7 +108,7 @@ def _select_charge_slots(
|
||||
default=min(float(s.buy_price) for s in slots),
|
||||
)
|
||||
ref_buy_global = min(float(s.buy_price) for s in slots)
|
||||
export_window_start = _export_window_start(slots, degrad)
|
||||
export_by_day = _export_window_start_by_day(slots, degrad)
|
||||
plan_day = _prague_date(slots[0])
|
||||
|
||||
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.0)
|
||||
@@ -161,12 +169,7 @@ def _select_charge_slots(
|
||||
|
||||
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, int, float, int]:
|
||||
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
|
||||
before_export = (
|
||||
0
|
||||
if export_window_start is not None
|
||||
and slots[t].interval_start < export_window_start
|
||||
else 1
|
||||
)
|
||||
before_export = 0 if _before_day_export(slots, t, export_by_day) else 1
|
||||
return (today_first, before_export, int(pred), price, t)
|
||||
|
||||
if purchase_pricing_mode != "fixed":
|
||||
@@ -733,6 +736,38 @@ class FixedPurchasePricingTests(unittest.TestCase):
|
||||
night = {t for t in out if _prague_hour(slots[t]) < 8}
|
||||
self.assertGreater(len(night), 0, "očekáváno grid nabíjení v noci před večerním výkupem")
|
||||
|
||||
def test_fixed_nt_charge_after_yesterday_evening_peak(self) -> None:
|
||||
"""Včerejší večerní peak nesmí zablokovat dnešní 00–06 grid (per-day export okno)."""
|
||||
base = datetime(2026, 5, 23, 20, 0, tzinfo=_PRAGUE)
|
||||
slots: list[PlanningSlot] = []
|
||||
for i in range(64):
|
||||
t = base + timedelta(minutes=15 * i)
|
||||
sell = 3.8 if t.hour >= 20 and t.date() == date(2026, 5, 23) else 2.9
|
||||
if t.date() == date(2026, 5, 24) and t.hour >= 19:
|
||||
sell = 3.7
|
||||
slots.append(
|
||||
_slot(
|
||||
buy=3.088,
|
||||
sell=sell,
|
||||
load=200,
|
||||
interval_start=t.astimezone(timezone.utc),
|
||||
)
|
||||
)
|
||||
battery = _battery(charge_buf=1.3, uc_wh=12_500.0)
|
||||
out = _select_charge_slots(
|
||||
slots,
|
||||
battery,
|
||||
current_soc_wh=0.33 * battery.usable_capacity_wh,
|
||||
purchase_pricing_mode="fixed",
|
||||
)
|
||||
may24_night = {
|
||||
t
|
||||
for t in out
|
||||
if _prague_date(slots[t]) == date(2026, 5, 24)
|
||||
and _prague_hour(slots[t]) < 6
|
||||
}
|
||||
self.assertGreater(len(may24_night), 0)
|
||||
|
||||
def test_fixed_allows_discharge_on_high_sell(self) -> None:
|
||||
slots = [
|
||||
_slot(buy=3.09, sell=1.0, hour_utc=10),
|
||||
|
||||
@@ -304,24 +304,33 @@ begin
|
||||
from _ems_plan_slot_wk wk
|
||||
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;
|
||||
|
||||
-- První výkupní okno: sell nad min(buy) **téhož kalendářního dne** (Prague), ne globální
|
||||
-- min(buy) zítra (NT) — jinak okno začne už ~15:30 a dnešní PM grid dostane 1 slot.
|
||||
select min(wk.interval_start)
|
||||
into v_export_window_start
|
||||
from _ems_plan_slot_wk wk
|
||||
where wk.sell_price > (
|
||||
select coalesce(min(w2.buy_price), wk.buy_price) + v_degrad_czk_kwh
|
||||
from _ems_plan_slot_wk w2
|
||||
where (w2.interval_start at time zone 'Europe/Prague')::date
|
||||
= (wk.interval_start at time zone 'Europe/Prague')::date
|
||||
);
|
||||
|
||||
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
|
||||
alter table _ems_plan_slot_wk
|
||||
add column if not exists future_sell_lookahead numeric,
|
||||
add column if not exists buy_min_next_n numeric,
|
||||
add column if not exists store_score numeric,
|
||||
add column if not exists allow_grid_charge boolean default false;
|
||||
add column if not exists allow_grid_charge boolean default false,
|
||||
add column if not exists export_window_start_at timestamptz;
|
||||
|
||||
-- První výkupní okno **per kalendářní den** (Prague). Globální min přes dny by
|
||||
-- zablokoval NT grid nabíjení (včerejší večerní peak → dnešní 00–06 už „po okně“).
|
||||
update _ems_plan_slot_wk wk
|
||||
set export_window_start_at = (
|
||||
select min(wx.interval_start)
|
||||
from _ems_plan_slot_wk wx
|
||||
where (wx.interval_start at time zone 'Europe/Prague')::date
|
||||
= (wk.interval_start at time zone 'Europe/Prague')::date
|
||||
and wx.sell_price > (
|
||||
select coalesce(min(w2.buy_price), wx.buy_price) + v_degrad_czk_kwh
|
||||
from _ems_plan_slot_wk w2
|
||||
where (w2.interval_start at time zone 'Europe/Prague')::date
|
||||
= (wx.interval_start at time zone 'Europe/Prague')::date
|
||||
)
|
||||
);
|
||||
|
||||
select min(wk.export_window_start_at)
|
||||
into v_export_window_start
|
||||
from _ems_plan_slot_wk wk;
|
||||
|
||||
update _ems_plan_slot_wk wk
|
||||
set
|
||||
@@ -339,8 +348,8 @@ begin
|
||||
where w2.slot_ord > wk.slot_ord
|
||||
and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
|
||||
and (
|
||||
v_export_window_start is null
|
||||
or w2.interval_start < v_export_window_start
|
||||
wk.export_window_start_at is null
|
||||
or w2.interval_start < wk.export_window_start_at
|
||||
)
|
||||
),
|
||||
store_score =
|
||||
@@ -431,8 +440,8 @@ begin
|
||||
else 1
|
||||
end,
|
||||
case
|
||||
when v_export_window_start is not null
|
||||
and wk.interval_start < v_export_window_start
|
||||
when wk.export_window_start_at is not null
|
||||
and wk.interval_start < wk.export_window_start_at
|
||||
then 0
|
||||
else 1
|
||||
end,
|
||||
@@ -479,8 +488,8 @@ begin
|
||||
else 1
|
||||
end,
|
||||
case
|
||||
when v_export_window_start is not null
|
||||
and wk.interval_start < v_export_window_start
|
||||
when wk.export_window_start_at is not null
|
||||
and wk.interval_start < wk.export_window_start_at
|
||||
then 0
|
||||
else 1
|
||||
end,
|
||||
@@ -522,8 +531,8 @@ begin
|
||||
else 1
|
||||
end,
|
||||
case
|
||||
when v_export_window_start is not null
|
||||
and wk.interval_start < v_export_window_start
|
||||
when wk.export_window_start_at is not null
|
||||
and wk.interval_start < wk.export_window_start_at
|
||||
then 0
|
||||
else 1
|
||||
end,
|
||||
@@ -567,8 +576,8 @@ begin
|
||||
else 1
|
||||
end,
|
||||
case
|
||||
when v_export_window_start is not null
|
||||
and wk.interval_start < v_export_window_start
|
||||
when wk.export_window_start_at is not null
|
||||
and wk.interval_start < wk.export_window_start_at
|
||||
then 0
|
||||
else 1
|
||||
end,
|
||||
|
||||
@@ -108,6 +108,18 @@ where pr.site_id = (select id from ems.site where code='BA81') and pr.status='ac
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-24 (d) — BA81: grid jen 1 slot (globální export okno)
|
||||
|
||||
**Problém:** Run **15820** — mírné zlepšení (1× ~4,5 kW grid+bat o půlnoci), ale **00:45–05:45 `allow_charge=false`**, max nabíjení pořád ~3,3 kW z FVE.
|
||||
|
||||
**Příčina:** `v_export_window_start` = **min přes celý horizont** (včerejší večerní sell 3,7 → čas ~22:15). Grid vrstva B řadí „před oknem“ vůči tomuto **jednomu** času → dnešní NT sloty (00–06) už jsou „po okně“ a nedostanou `allow_grid_charge`.
|
||||
|
||||
**Oprava:** Sloupec **`export_window_start_at` per kalendářní den** (Prague); grid AM/PM i `buy_min_next_n` používají `wk.interval_start < wk.export_window_start_at`.
|
||||
|
||||
**Deploy:** `flyway migrate` (R__063) + replan.
|
||||
|
||||
---
|
||||
|
||||
## Šablona pro další záznamy
|
||||
|
||||
```markdown
|
||||
|
||||
Reference in New Issue
Block a user