feat(forecast): commission corrected Lihrenmoos profile
This commit is contained in:
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user