import xml.etree.ElementTree as ET
from pathlib import Path
CIM_NS = "http://iec.ch/TC57/CIM100#"
RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
def parse_sv_voltages(sv_path: Path) -> dict[str, tuple[float, float]]:
tree = ET.parse(sv_path)
out = {}
for sv in tree.getroot().findall(f"{{{CIM_NS}}}SvVoltage"):
tn = sv.find(f"{{{CIM_NS}}}SvVoltage.TopologicalNode")
v = sv.find(f"{{{CIM_NS}}}SvVoltage.v")
angle = sv.find(f"{{{CIM_NS}}}SvVoltage.angle")
if tn is None or v is None or angle is None:
continue
tn_id = "_" + tn.get(f"{{{RDF_NS}}}resource", "").lstrip("#_")
out[tn_id] = (float(v.text), float(angle.text))
return out
def parse_tn_containers(tp_path: Path) -> dict[str, str]:
tree = ET.parse(tp_path)
out = {}
for tn in tree.getroot().findall(f"{{{CIM_NS}}}TopologicalNode"):
tn_id = "_" + tn.get(f"{{{RDF_NS}}}ID", "").lstrip("_")
container = tn.find(f"{{{CIM_NS}}}TopologicalNode.ConnectivityNodeContainer")
if container is not None:
container_id = container.get(f"{{{RDF_NS}}}resource", "").lstrip("#_")
out[tn_id] = container_id
return out
def match_powsybl_buses_to_tn(
tn_to_container: dict[str, str],
expected: dict[str, tuple[float, float]],
powsybl_buses,
) -> dict[str, str]:
by_container: dict[str, list[str]] = {}
for tn_id, container_id in tn_to_container.items():
if tn_id in expected:
by_container.setdefault(container_id, []).append(tn_id)
powsybl_by_container: dict[str, list[tuple[str, float]]] = {}
for bus_id, v_mag in powsybl_buses["v_mag"].items():
container_id, _, _idx = bus_id.rpartition("_")
powsybl_by_container.setdefault(container_id, []).append((bus_id, v_mag))
out = {}
for container_id, tn_ids in by_container.items():
candidates = list(powsybl_by_container.get(container_id, []))
for tn_id in tn_ids:
if not candidates:
continue
exp_kv, _ = expected[tn_id]
best = min(candidates, key=lambda c: abs(c[1] - exp_kv))
candidates.remove(best)
out[tn_id] = best[0]
return out
def deviation_stats(errors: list[float]) -> dict[str, float]:
if not errors:
return {"n": 0, "median": float("nan"), "p90": float("nan"), "max": float("nan")}
errs = sorted(errors)
n = len(errs)
return {
"n": n,
"median": errs[n // 2],
"p90": errs[n * 9 // 10],
"max": errs[-1],
}