v2: měkký EV cíl — oportunistické nabíjení nad target (+ strop energie)
Uživatel: 'potřebuju do X % (tvrdý), ale klidně dobij na 100 % když je to skoro zadarmo; při záporných cenách radši do auta než nechat na střeše'. - V094 asset_vehicle.opportunistic_value_czk_kwh (default 1.0; = hodnota ušetřeného BUDOUCÍHO nabíjení — auto neumí zpět, žádný noční prodej) - R__039 ev_sessions: + headroom_wh ((100−target) % kapacity) + opp value; session se nenuluje po dosažení targetu, dokud má headroom - solver_v2: dekompozice Σ(EV) == needed − unmet + opp, opp ∈ [0, headroom], odměna opp×value; zároveň FIX latentního bugu — při buy<0 chyběl strop celkové energie do auta (model mohl pumpovat bez limitu) - 3 testy (neg ceny sají nad target po strop; běžné ceny ne; cap při opp=0); eval fixtures beze změny (sessions null) Víkend (pátek nízký tvrdý cíl + víkendová negativa → samo doplní do 100 %) vyplývá z mechanismu, žádná speciální logika. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -247,6 +247,41 @@ class PvRiskFrontloadTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class EvOpportunisticTests(unittest.TestCase):
|
||||
def _session(self, needed=4000.0, headroom=20000.0, opp=1.0):
|
||||
return SimpleNamespace(
|
||||
target_deadline=_BASE + timedelta(hours=2),
|
||||
energy_needed_wh=needed,
|
||||
headroom_wh=headroom,
|
||||
opportunistic_value_czk_kwh=opp,
|
||||
)
|
||||
|
||||
def test_negative_prices_fill_beyond_target(self) -> None:
|
||||
# buy<0 celé okno → nad target se vyplatí brát (hodnota 1 Kč/kWh + platí ti síť)
|
||||
slots = [_slot(_BASE, i, buy=-1.0, sell=-0.5, ev1=True, load=300) for i in range(16)]
|
||||
results, _, snap = _solve(slots, ev_sessions=(self._session(), None))
|
||||
delivered = sum((r.ev1_setpoint_w or 0) * 0.25 for r in results)
|
||||
self.assertGreater(delivered, 4000.0 + 2000.0, "měkký cíl má nasávat")
|
||||
self.assertLessEqual(delivered, 4000.0 + 20000.0 + 1.0, "strop headroom")
|
||||
self.assertGreater(snap["objective_terms"]["ev_opp_wh"][0], 0)
|
||||
|
||||
def test_normal_prices_no_opportunistic(self) -> None:
|
||||
# běžné ceny (buy 3) > hodnota 1 Kč/kWh → jen tvrdý cíl
|
||||
slots = [_slot(_BASE, i, buy=3.0, sell=2.0, ev1=True, load=300) for i in range(16)]
|
||||
results, _, snap = _solve(slots, ev_sessions=(self._session(), None))
|
||||
delivered = sum((r.ev1_setpoint_w or 0) * 0.25 for r in results)
|
||||
self.assertLess(delivered, 4000.0 + 200.0)
|
||||
self.assertLess(snap["objective_terms"]["ev_opp_wh"][0], 100.0)
|
||||
|
||||
def test_total_energy_capped_even_at_negative_buy(self) -> None:
|
||||
# fix latentního bugu: bez headroom (opp=0) nesmí buy<0 pumpovat nad needed
|
||||
slots = [_slot(_BASE, i, buy=-2.0, sell=-1.0, ev1=True, load=300) for i in range(16)]
|
||||
sess = self._session(needed=3000.0, headroom=0.0, opp=0.0)
|
||||
results, _, _ = _solve(slots, ev_sessions=(sess, None))
|
||||
delivered = sum((r.ev1_setpoint_w or 0) * 0.25 for r in results)
|
||||
self.assertLessEqual(delivered, 3000.0 + 1.0)
|
||||
|
||||
|
||||
class EvDeadlineTests(unittest.TestCase):
|
||||
def test_ev_energy_delivered_before_deadline(self) -> None:
|
||||
slots = [_slot(_BASE, i, buy=2.0 if i < 8 else 6.0, sell=1.0, ev1=True) for i in range(16)]
|
||||
|
||||
Reference in New Issue
Block a user