feat(forecast): commission corrected Lihrenmoos profile

This commit is contained in:
dh
2026-10-04 09:07:30 +00:00
parent 913a4cd5f3
commit 30f7652803
5 changed files with 393 additions and 3 deletions
@@ -0,0 +1,116 @@
import importlib.util
import json
import sqlite3
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"activate_corrected_forecast", ROOT / "commissioning/activate_corrected_forecast.py"
)
module = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(module)
from netplan_v4 import measurement_pipeline
from netplan_v4.store import PlannerStore
PLANT = module.PLANT
DATASET = module.DATASET
NOW = 1791099600
class CorrectedForecastCommissioningTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db = Path(self.tmp.name) / "planner.sqlite"
self.store = PlannerStore(str(self.db))
config = {
"datasetId": DATASET,
"mappingSha256": "a" * 64,
"inventorySha256": "b" * 64,
"formula": "physical_sum_v1",
"solarReference": None,
"minimumCoverage": 0.95,
"maximumGapSeconds": 10,
"minimumTrainingHours": 24,
"historyDays": 14,
"sources": [
{"key": "grid", "variableId": 1, "role": "grid", "factorToW": 1, "maxAgeSeconds": 60},
{"key": "pv", "variableId": 2, "role": "pv", "factorToW": 1, "maxAgeSeconds": 60},
{"key": "battery", "variableId": 3, "role": "physical_storage", "factorToW": 1, "maxAgeSeconds": 60},
],
}
measurement_pipeline.register_dataset(self.store.con, PLANT, config, NOW - 100)
model = {
"version": 1,
"datasetId": DATASET,
"methodVersion": "profile_v1",
"trainedAt": NOW - 60,
"trainedThrough": NOW - 300,
"profiles": {},
"validation": {"status": "bootstrap_insufficient_holdout"},
}
model_id = "c" * 64
with self.store.con:
self.store.con.execute(
"INSERT INTO planner_load_models VALUES(?,?,?,?,?,?)",
(PLANT, DATASET, model_id, NOW - 60, NOW - 300, json.dumps(model)),
)
self.store.con.execute(
"INSERT INTO planner_model_current VALUES(?,?,?)", (PLANT, DATASET, model_id)
)
self.store.con.execute(
"INSERT INTO planner_pipeline_state VALUES(?,?,?,?,?)",
(
PLANT,
DATASET,
1,
"model_ready",
json.dumps(
{
"modelId": model_id,
"usableEquivalentHours": 24.1,
"requiredEquivalentHours": 24,
}
),
),
)
self.store.con.execute(
"INSERT INTO planner_observations VALUES(?,?,?,?,?,?)",
(PLANT, DATASET, NOW - 30, NOW - 20, "d" * 64, "{}"),
)
self.store.close()
def tearDown(self):
self.tmp.cleanup()
def test_ready_state_can_activate_only_corrected_shadow_settings(self):
state = module.read_state(self.db, PLANT, DATASET, NOW)
module.require_ready(state, NOW)
result = module.activate(
self.db, PLANT, DATASET, datetime.fromtimestamp(NOW, timezone.utc)
)
self.assertTrue(result["changed"])
store = PlannerStore(str(self.db))
settings = store.settings(PLANT)
self.assertEqual(settings["forecastSource"], "corrected_profile")
self.assertEqual(settings["measurementDataset"], DATASET)
self.assertEqual(settings["runMode"], "shadow")
store.close()
def test_stale_observations_refused(self):
state = module.read_state(self.db, PLANT, DATASET, NOW + 700)
with self.assertRaises(ValueError):
module.require_ready(state, NOW + 700)
def test_backup_is_consistent(self):
backup = Path(self.tmp.name) / "backup.sqlite"
module.backup_database(self.db, backup)
with sqlite3.connect(backup) as con:
self.assertEqual(con.execute("PRAGMA integrity_check").fetchone()[0], "ok")
if __name__ == "__main__":
unittest.main()
@@ -67,6 +67,9 @@ class MeasurementPipelineTest(unittest.TestCase):
def test_no_naive_time(self):
r=record(NOW);r['capturedAt']='2026-10-02T12:00:00'
with self.assertRaises(ValueError):self.batch([r])
def test_measurement_time_remains_whole_second(self):
r=record(NOW);r['capturedAt']='2026-10-02T12:00:00.000001+00:00'
with self.assertRaises(ValueError):self.batch([r])
def test_future_source_not_rejuvenated(self):
r=record(NOW);r['raw']['grid']['sourceUpdatedAt']=NOW+1
self.assertFalse(m.project(r,self.c,AID,NOW)['raw']['grid']['valid'])
@@ -115,7 +118,10 @@ class MeasurementPipelineTest(unittest.TestCase):
with self.assertRaises(ValueError):m.apply_load_forecast(self.s.con,AID,'physical-v1',fc,NOW)
def test_external_is_labelled_persistence_not_published(self):
self.history();m.advance(self.s.con,AID,'physical-v1',self.s.settings(AID),NOW)
op,fc,tar=inputs(datetime.fromtimestamp(NOW,timezone.utc));out=m.apply_load_forecast(self.s.con,AID,'physical-v1',fc,NOW)
op,fc,tar=inputs(datetime.fromtimestamp(NOW,timezone.utc))
fc['observedAt']=datetime.fromtimestamp(NOW-1,timezone.utc).replace(microsecond=1).isoformat()
out=m.apply_load_forecast(self.s.con,AID,'physical-v1',fc,NOW)
self.assertEqual(out['observedAt'],m.iso(NOW))
self.assertEqual(out['families']['3']['points'][0]['loadW'],6000)
self.assertFalse(out['families']['3']['dataPipeline']['futureSdlPublished'])
self.assertNotIn('accountingEvidenceId',out['families']['3'])