rezani poole i kdyz je zlenenobonusove pole na stejnmstridaci
Some checks failed
CI and deploy / migration-check (push) Failing after 16s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-01 13:01:49 +02:00
parent 1e0300dd7e
commit e54eb1dfd9
3 changed files with 74 additions and 2 deletions

View File

@@ -562,6 +562,18 @@ def solve_dispatch(
# by to jinak vedlo k nežádoucímu exportu / infeasible řešení.
GEN_CUTOFF_PENALTY_CZK_KWH = 5.0
# Heuristika: pokud existuje necurtailable PV B a v budoucnu v horizontu nastane buy < 0,
# chceme mít motivaci držet baterii „prázdnější“ pro pozdější výhodný import / bonusové PV B okno.
# V okně sell < 0 pak preferujeme curtail PV A (místo placeného exportu), a to tak,
# že dočasně snížíme penalizaci ca[t] (curtailment) na 0.
has_pv_b = any(float(s.pv_b_forecast_w) > 0.0 for s in slots)
future_neg_buy_from: list[bool] = [False] * T
seen_neg_buy = False
for i in range(T - 1, -1, -1):
if float(slots[i].buy_price) < 0.0:
seen_neg_buy = True
future_neg_buy_from[i] = seen_neg_buy
# EV proměnné per vozidlo
ev_direct = [[pulp.LpVariable(f"evd_{e}_{t}", 0,
min(vehicles[e].max_charge_power_w, grid.max_import_power_w))
@@ -611,7 +623,16 @@ def solve_dispatch(
+ ev_via_bat[e][t] * slots[t].buy_price * EV_ROUNDTRIP_FACTOR * INTERVAL_H / 1000
for e in range(EV)
)
+ ca[t] * CURTAILMENT_PENALTY
+ ca[t]
* (
0.0
if (
has_pv_b
and future_neg_buy_from[t]
and float(slots[t].sell_price) < 0.0
)
else CURTAILMENT_PENALTY
)
for t in range(T)
)
+ soc_deficit_24h * soc_deficit_penalty_czk_kwh / 1000

View File

@@ -205,6 +205,55 @@ def replace_slot(
class PlanningDispatchMilpTests(unittest.TestCase):
def test_neg_sell_with_future_neg_buy_prefers_curtail_pv_a_over_export(self) -> None:
"""
Když:
- aktuální slot má sell < 0 (export je náklad),
- v horizontu existuje budoucí buy < 0,
- a zároveň existuje PV B (necurtailable) někde v horizontu,
solver preferuje curtail PV A (ca) místo placeného exportu ge.
"""
slots = [
_slot(load=0, buy=3.0, sell=-0.1, pv_a=5000, pv_b=0),
_slot(load=0, buy=-10.0, sell=1.0, pv_a=0, pv_b=5000),
]
battery = _battery(uc_wh=50_000.0)
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=20_000, max_export_power_w=20_000)
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,
),
]
soc0 = 0.50 * battery.usable_capacity_wh
results, _ms = solve_dispatch(
slots,
battery,
hp,
grid,
[None, None],
vehicles,
soc0,
50.0,
tuv_delta_stats=None,
operating_mode="AUTO",
)
self.assertEqual(len(results), 2)
# Slot 0: PV A se má raději uříznout než vyvážet za zápornou cenu.
self.assertEqual(int(results[0].pv_a_curtailed_w), 5000)
self.assertGreaterEqual(int(results[0].grid_setpoint_w), 0)
def test_two_tier_soc_solves_optimal(self) -> None:
slots = [_slot()]
battery = _battery()

View File

@@ -150,7 +150,9 @@ minimize:
# Solver tak přirozeně preferuje přímé nabíjení nad průchodem baterií
+ Σ_e ev_via_bat[e][t] * buy_price[t] * EV_ROUNDTRIP_FACTOR * interval_h
# Malá penalizace curtailmentu pole A (preferujeme využití FVE)
# Malá penalizace curtailmentu pole A (preferujeme využití FVE).
# Výjimka: pokud existuje PV B a v budoucnu v horizontu nastane buy < 0, pak v okně sell < 0
# solver preferuje curtail PV A před placeným exportem (penalizace curtailmentu se v těchto slotech snižuje na 0).
+ pv_a_curtailed[t] * CURTAILMENT_PENALTY
]
```