feat(scada): add calibrated monitoring and incident recovery
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate persisted isolated burst scenarios and mapped SCADA observations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--design", type=Path, default=Path("resources/benchmarks/tjwater2-v2-burst-balanced.json"))
|
||||
parser.add_argument("--target", default="tjw_bench_tjwater2_20261105")
|
||||
parser.add_argument("--state-dir", type=Path, default=Path("data/benchmark-generation/tjwater2-20261105"))
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
parser.add_argument("--prepare-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
os.umask(0o077)
|
||||
from psycopg import connect
|
||||
from psycopg.conninfo import make_conninfo
|
||||
from psycopg.rows import dict_row
|
||||
from app.core.config import settings
|
||||
from scripts.benchmark.calibration import save_json, digest
|
||||
from scripts.benchmark.generation import (
|
||||
apply_project_runtime_migrations,
|
||||
create_empty_database,
|
||||
copy_project_inputs,
|
||||
make_schedule,
|
||||
register_benchmark_project,
|
||||
)
|
||||
from scripts.benchmark.simulation_design import load_simulation_design
|
||||
from scripts.check_burst_simulation_design import resolve_project, readonly, validate_database
|
||||
|
||||
design = load_simulation_design(args.design)
|
||||
schedule = make_schedule(design)
|
||||
if not args.apply:
|
||||
print(f"dry run: {len(schedule)} cases, target={args.target}, dates={schedule[0]['history_start']}..{schedule[-1]['reserved_end']}")
|
||||
return 0
|
||||
folder = args.state_dir.resolve()
|
||||
folder.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
state_path = folder / "state.json"
|
||||
import json
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else {
|
||||
"version": "burst-dataset-generation@1", "created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"target": args.target, "design_sha256": digest(args.design), "schedule": schedule,
|
||||
"stages": {}, "cases": {}, "source_effect": "read-only", "status": "preparing"}
|
||||
if state["target"] != args.target or state["design_sha256"] != digest(args.design):
|
||||
raise ValueError("state belongs to a different target or design")
|
||||
save_json(state_path, state)
|
||||
biz, iot = asyncio.run(resolve_project(design))
|
||||
with readonly(biz) as connection:
|
||||
validate_database(connection, design, template=False)
|
||||
target_biz, target_iot = make_conninfo(biz, dbname=args.target), make_conninfo(iot, dbname=args.target)
|
||||
for role, source, template in (("biz", biz, settings.WNDB_SCHEMA_TEMPLATE_DB_NAME),
|
||||
("iot", iot, settings.TIMESCALEDB_SCHEMA_TEMPLATE_DB_NAME)):
|
||||
if not state["stages"].get(f"{role}_created"):
|
||||
create_empty_database(source, args.target, template)
|
||||
state["stages"][f"{role}_created"] = True
|
||||
save_json(state_path, state)
|
||||
print(f"created isolated {role} database: {args.target}", flush=True)
|
||||
if not state["stages"].get("model_copied"):
|
||||
state["model_counts"] = copy_project_inputs(biz, target_biz)
|
||||
state["stages"]["model_copied"] = True
|
||||
save_json(state_path, state)
|
||||
print("copied model and device bindings from a read-only source snapshot", flush=True)
|
||||
required_migrations = {
|
||||
"025_mcp_query_contexts.sql",
|
||||
"026_calculation_jobs.sql",
|
||||
"027_calculation_job_lifecycle.sql",
|
||||
"030_remove_calculation_queue_expiry.sql",
|
||||
}
|
||||
if not required_migrations.issubset(state.get("runtime_migrations", [])):
|
||||
state["runtime_migrations"] = apply_project_runtime_migrations(
|
||||
target_biz, Path(__file__).resolve().parents[1]
|
||||
)
|
||||
state["stages"]["runtime_migrations_applied"] = True
|
||||
save_json(state_path, state)
|
||||
print("installed isolated MCP and calculation queue storage", flush=True)
|
||||
if not state["stages"].get("model_template_created"):
|
||||
from app.native.wndb.core.project_templates import (
|
||||
create_project_model_template,
|
||||
ensure_replication_worker_capacity,
|
||||
)
|
||||
from app.native.wndb.core.projects import have_project
|
||||
|
||||
template = f"{args.target}_template"
|
||||
if not have_project(template):
|
||||
try:
|
||||
ensure_replication_worker_capacity()
|
||||
except RuntimeError as exc:
|
||||
if "no reserved logical-replication worker capacity" not in str(exc):
|
||||
raise
|
||||
state["model_template_mode"] = "snapshot_per_run"
|
||||
else:
|
||||
create_project_model_template(args.target)
|
||||
if have_project(template):
|
||||
state["model_template"] = template
|
||||
state["model_template_mode"] = "logical_replication"
|
||||
state["stages"]["model_template_created"] = True
|
||||
save_json(state_path, state)
|
||||
print(f"configured hydraulic model isolation: {state['model_template_mode']}", flush=True)
|
||||
if not state["stages"].get("metadata_registered"):
|
||||
project = asyncio.run(register_benchmark_project(
|
||||
source_project_id=str(design.project_id),
|
||||
target=args.target,
|
||||
target_biz_dsn=target_biz,
|
||||
target_iot_dsn=target_iot,
|
||||
))
|
||||
state["project_id"] = str(project.id)
|
||||
state["stages"]["metadata_registered"] = True
|
||||
save_json(state_path, state)
|
||||
print(f"registered isolated benchmark project: {project.id}", flush=True)
|
||||
# Inspect isolated schema readiness before invoking backend persistence.
|
||||
with connect(target_biz, row_factory=dict_row) as connection:
|
||||
print("isolated BizDB analysis schema", connection.execute(
|
||||
"SELECT column_name FROM information_schema.columns WHERE table_schema='analysis' AND table_name='runs' ORDER BY ordinal_position").fetchall(), flush=True)
|
||||
state["status"] = "prepared"
|
||||
save_json(state_path, state)
|
||||
print(f"isolated preparation complete: {state_path}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user