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()
|
return s.interval_start.astimezone(_PRAGUE).date()
|
||||||
|
|
||||||
|
|
||||||
def _export_window_start(slots: list[PlanningSlot], degrad: float) -> datetime | None:
|
def _export_window_start_by_day(
|
||||||
"""Kopie R__063: sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
|
slots: list[PlanningSlot], degrad: float
|
||||||
result: datetime | None = None
|
) -> 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:
|
for s in slots:
|
||||||
day = _prague_date(s)
|
day = _prague_date(s)
|
||||||
day_min = min(
|
day_min = min(float(x.buy_price) for x in slots if _prague_date(x) == day)
|
||||||
float(x.buy_price) for x in slots if _prague_date(x) == day
|
|
||||||
)
|
|
||||||
if float(s.sell_price) > day_min + degrad:
|
if float(s.sell_price) > day_min + degrad:
|
||||||
if result is None or s.interval_start < result:
|
prev = out.get(day)
|
||||||
result = s.interval_start
|
if prev is None or s.interval_start < prev:
|
||||||
return result
|
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:
|
def _future_sell(slots: list[PlanningSlot], t: int) -> float:
|
||||||
@@ -55,13 +63,13 @@ def _buy_min_next_n(
|
|||||||
t: int,
|
t: int,
|
||||||
n: int = _LOOKAHEAD_SLOTS,
|
n: int = _LOOKAHEAD_SLOTS,
|
||||||
*,
|
*,
|
||||||
export_window_start: datetime | None = None,
|
export_by_day: dict[date, datetime] | None = None,
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
tail = [
|
tail: list[float] = []
|
||||||
float(slots[i].buy_price)
|
for i in range(t + 1, min(t + 1 + n, len(slots))):
|
||||||
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 export_window_start is None or slots[i].interval_start < export_window_start
|
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
|
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),
|
default=min(float(s.buy_price) for s in slots),
|
||||||
)
|
)
|
||||||
ref_buy_global = 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])
|
plan_day = _prague_date(slots[0])
|
||||||
|
|
||||||
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.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]:
|
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
|
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
|
||||||
before_export = (
|
before_export = 0 if _before_day_export(slots, t, export_by_day) else 1
|
||||||
0
|
|
||||||
if export_window_start is not None
|
|
||||||
and slots[t].interval_start < export_window_start
|
|
||||||
else 1
|
|
||||||
)
|
|
||||||
return (today_first, before_export, int(pred), price, t)
|
return (today_first, before_export, int(pred), price, t)
|
||||||
|
|
||||||
if purchase_pricing_mode != "fixed":
|
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}
|
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")
|
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:
|
def test_fixed_allows_discharge_on_high_sell(self) -> None:
|
||||||
slots = [
|
slots = [
|
||||||
_slot(buy=3.09, sell=1.0, hour_utc=10),
|
_slot(buy=3.09, sell=1.0, hour_utc=10),
|
||||||
|
|||||||
@@ -304,24 +304,33 @@ begin
|
|||||||
from _ems_plan_slot_wk wk
|
from _ems_plan_slot_wk wk
|
||||||
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;
|
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.
|
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
|
||||||
alter table _ems_plan_slot_wk
|
alter table _ems_plan_slot_wk
|
||||||
add column if not exists future_sell_lookahead numeric,
|
add column if not exists future_sell_lookahead numeric,
|
||||||
add column if not exists buy_min_next_n numeric,
|
add column if not exists buy_min_next_n numeric,
|
||||||
add column if not exists store_score 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
|
update _ems_plan_slot_wk wk
|
||||||
set
|
set
|
||||||
@@ -339,8 +348,8 @@ begin
|
|||||||
where w2.slot_ord > wk.slot_ord
|
where w2.slot_ord > wk.slot_ord
|
||||||
and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
|
and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
|
||||||
and (
|
and (
|
||||||
v_export_window_start is null
|
wk.export_window_start_at is null
|
||||||
or w2.interval_start < v_export_window_start
|
or w2.interval_start < wk.export_window_start_at
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
store_score =
|
store_score =
|
||||||
@@ -431,8 +440,8 @@ begin
|
|||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when wk.export_window_start_at is not null
|
||||||
and wk.interval_start < v_export_window_start
|
and wk.interval_start < wk.export_window_start_at
|
||||||
then 0
|
then 0
|
||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
@@ -479,8 +488,8 @@ begin
|
|||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when wk.export_window_start_at is not null
|
||||||
and wk.interval_start < v_export_window_start
|
and wk.interval_start < wk.export_window_start_at
|
||||||
then 0
|
then 0
|
||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
@@ -522,8 +531,8 @@ begin
|
|||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when wk.export_window_start_at is not null
|
||||||
and wk.interval_start < v_export_window_start
|
and wk.interval_start < wk.export_window_start_at
|
||||||
then 0
|
then 0
|
||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
@@ -567,8 +576,8 @@ begin
|
|||||||
else 1
|
else 1
|
||||||
end,
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when wk.export_window_start_at is not null
|
||||||
and wk.interval_start < v_export_window_start
|
and wk.interval_start < wk.export_window_start_at
|
||||||
then 0
|
then 0
|
||||||
else 1
|
else 1
|
||||||
end,
|
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
|
## Šablona pro další záznamy
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|||||||
Reference in New Issue
Block a user