feat(forecast): integrate full forecast charts and complete SDL visualization
Tests / test (push) Failing after 52s

This commit is contained in:
dh
2026-10-06 17:40:45 +00:00
parent 5687596799
commit 372b997876
30 changed files with 4377 additions and 53 deletions
+19
View File
@@ -36,6 +36,16 @@ def inputs(at=AT):
return op,forecast,tariff
class OptimizerTest(unittest.TestCase):
def test_plan_preserves_exact_forecast_components(self):
steps = [replace(s, external_w=725.) for s in seq([2100., 2200., 2300.], pv=[3100., 3200., 3300.])]
plan = solve(steps)
self.assertTrue(plan['executable'])
for step, point in zip(steps, plan['points']):
self.assertEqual(point['pvW'], step.pv_w)
self.assertEqual(point['loadW'], step.base_load_w)
self.assertEqual(point['externalW'], step.external_w)
self.assertAlmostEqual(point['baselineGridW'], point['loadW'] + point['externalW'] - point['pvW'])
def test_grid_price_arbitrage(self):
p=solve(seq([1000.]*6,buy=[.01]*3+[.7]*3),[batt()]);self.assertTrue(p['executable']);self.assertGreater(p['points'][0]['batteryTargetW'],0);self.assertLess(p['points'][-1]['batteryTargetW'],0)
def test_no_future_energy(self):
@@ -157,12 +167,21 @@ class ServiceIntegrationTest(unittest.TestCase):
op,fc,tar=inputs();tar['import']={'mode':'dynamic','tariffId':'buy'};self.load((op,fc,tar))
price={'version':1,'eventId':'price1','observedAt':AT.isoformat(),'periods':[{'tariffId':'buy','side':'import','start':AT.isoformat(),'end':(AT+timedelta(minutes=15)).isoformat(),'value':-2.,'unit':'Rp/kWh','observedAt':AT.isoformat(),'sourceKind':'published_interval'}]}
ingest(self.store,AID,'prices',price,AT);p=run_once(self.store,AT);self.assertEqual(len(p['points']),3);self.assertEqual(p['points'][0]['importPriceChfKwh'],-.02)
self.assertEqual(len(p['forecastPoints']), 6)
self.assertEqual(p['forecastPoints'][-1]['validUntil'], p['forecastUntil'])
self.assertGreater(p['forecastUntil'], p['pricesKnownUntil'])
self.assertNotIn('gridTargetW', p['forecastPoints'][-1])
self.assertNotIn('forecastPoints', p['inputQuality'])
def test_carried_price_does_not_extend_horizon(self):
op,fc,tar=inputs();tar['import']={'mode':'dynamic','tariffId':'buy'};self.load((op,fc,tar))
price={'version':1,'eventId':'price1','observedAt':AT.isoformat(),'periods':[{'tariffId':'buy','side':'import','start':AT.isoformat(),'end':(AT+timedelta(hours=48)).isoformat(),'value':.1,'unit':'CHF/kWh','observedAt':AT.isoformat(),'sourceKind':'carried_forward'}]}
ingest(self.store,AID,'prices',price,AT);self.assertEqual(run_once(self.store,AT)['status'],'awaiting_inputs')
def test_aggregate_load_warning(self):
op,fc,tar=inputs();fc['families']['3']['loadBasis']='house_total';self.load((op,fc,tar));self.assertTrue(run_once(self.store,AT)['warnings'])
def test_aggregate_forecast_does_not_invent_separate_sdl(self):
op,fc,tar=inputs();fc['families']['3']['loadBasis']='house_total'
self.load((op,fc,tar));plan=run_once(self.store,AT)
self.assertTrue(all(point['externalW'] is None for point in plan['forecastPoints']))
def test_extra_credentials_not_stored(self):
op,fc,tar=inputs();op['password']='do-not-store'
with self.assertRaises(ValueError):ingest(self.store,AID,'operation',op,AT)