import glob
import os
import sys
ORIGINALS = {
"opendss_ieee13_IEEE13Nodeckt": "tests/data/dist/opendss/ieee13/IEEE13Nodeckt.dss",
"opendss_ieee34_ieee34Mod1": "tests/data/dist/opendss/ieee34/ieee34Mod1.dss",
"opendss_ieee123_IEEE123Master": "tests/data/dist/opendss/ieee123/IEEE123Master.dss",
"micro_xfmr_single_phase": "tests/data/dist/micro/xfmr_single_phase.dss",
"micro_xfmr_center_tap": "tests/data/dist/micro/xfmr_center_tap.dss",
"micro_xfmr_wye_delta": "tests/data/dist/micro/xfmr_wye_delta.dss",
"micro_xfmr_delta_wye": "tests/data/dist/micro/xfmr_delta_wye.dss",
"micro_xfmr_open_wye_open_delta": "tests/data/dist/micro/xfmr_open_wye_open_delta.dss",
"micro_xfmr_1ph_delta_wye": "tests/data/dist/micro/xfmr_1ph_delta_wye.dss",
"micro_switch": "tests/data/dist/micro/switch.dss",
"micro_fourwire_linecode": "tests/data/dist/micro/fourwire_linecode.dss",
"micro_defaults_degenerate": "tests/data/dist/micro/defaults_degenerate.dss",
"micro_linecode_10x10": "tests/data/dist/micro/linecode_10x10.dss",
}
def solve(path):
import opendssdirect as dss
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
lines = text.splitlines()
injected = False
for i, line in enumerate(lines):
head = line.lower().lstrip()
if head.startswith("new circuit") or head.startswith("new object=circuit"):
lines.insert(i + 1, "Set Controlmode=OFF")
lines.insert(i + 2, "Set tolerance=0.0000000001")
injected = True
break
if not injected:
raise SystemExit(f"{path}: no circuit definition found to stage")
staged = os.path.join(os.path.dirname(os.path.abspath(path)), "_staged_" + os.path.basename(path))
with open(staged, "w") as f:
f.write("\n".join(lines) + "\n")
try:
dss.Text.Command("Clear")
dss.Text.Command(f'Redirect "{os.path.abspath(staged)}"')
dss.Text.Command("Set Controlmode=OFF")
dss.Text.Command("Solve")
finally:
os.unlink(staged)
if not dss.Solution.Converged():
return None
volts = {}
for bus in dss.Circuit.AllBusNames():
dss.Circuit.SetActiveBus(bus)
nodes = dss.Bus.Nodes()
raw = dss.Bus.Voltages()
for k, node in enumerate(nodes):
volts[f"{bus}.{node}"] = complex(raw[2 * k], raw[2 * k + 1])
return volts
def compare(base, emitted):
bus_base = {}
for node, v0 in base.items():
bus = node.rsplit(".", 1)[0]
bus_base[bus] = max(bus_base.get(bus, 0.0), abs(v0))
worst = 0.0
worst_node = ""
for node, v0 in base.items():
v1 = emitted.get(node)
if v1 is None:
return None, f"missing node {node}"
base_v = max(bus_base[node.rsplit(".", 1)[0]], 1.0)
dev = abs(v1 - v0) / base_v
if dev > worst:
worst, worst_node = dev, node
return worst, worst_node
DOCUMENTED = {
("micro_defaults_degenerate", "canonical"): (1e-6, "engine seeding asymmetry"),
("micro_defaults_degenerate", "via_pmd"): (1e-6, "engine seeding asymmetry"),
("micro_defaults_degenerate", "via_bmopf"): (1e-2, "BMOPF: constant power loads only"),
("opendss_ieee13_IEEE13Nodeckt", "via_bmopf"): (1e-1, "BMOPF: constant power loads only"),
("opendss_ieee34_ieee34Mod1", "via_bmopf"): (1e-1, "BMOPF: constant power loads only"),
("opendss_ieee34_ieee34Mod1", "via_pmd"): (1e-1, "no vminpu field in ENGINEERING"),
("opendss_ieee123_IEEE123Master", "via_bmopf"): (None, "transformer shape outside the four BMOPF subtypes"),
("opendss_ieee123_IEEE123Master", "via_pmd"): (1e-2, "regulator bank restatement"),
("micro_xfmr_center_tap", "via_bmopf"): (2e-1, "BMOPF: center tap collapses to two windings"),
("micro_xfmr_open_wye_open_delta", "via_bmopf"): (2e-1, "BMOPF: open delta floating reference, no wye/delta label"),
("micro_xfmr_open_wye_open_delta", "via_pmd"): (1.5e0, "PMD: single phase delta winding read as two phase"),
("micro_xfmr_1ph_delta_wye", "via_bmopf"): (1e-3, "BMOPF: single phase delta winding impedance base, no wye/delta label"),
("micro_xfmr_1ph_delta_wye", "via_pmd"): (1e0, "PMD: single phase delta winding read as two phase"),
("micro_xfmr_single_phase", "via_pmd"): (1e-6, "engine Z1/Z0 vs MVAsc input path"),
("micro_switch", "via_pmd"): (1e-5, "ENGINEERING switch impedance convention"),
("micro_xfmr_center_tap", "via_pmd"): (1e-6, "engine Z1/Z0 vs MVAsc input path"),
}
def main():
failures = 0
for stem, original in ORIGINALS.items():
emitted_paths = sorted(glob.glob(f"target/physics/{stem}.*.dss"))
if not emitted_paths:
print(f"{stem}: NO EMITTED CASES under target/physics (run the emit test first)")
failures += 1
continue
base = solve(original)
if base is None:
print(f"{stem}: ORIGINAL DID NOT CONVERGE")
failures += 1
continue
for emitted_path in emitted_paths:
kind = emitted_path.rsplit(".", 2)[-2]
bound, reason = DOCUMENTED.get((stem, kind), (1e-8, None))
emitted = solve(emitted_path)
if emitted is None:
print(f"{stem} [{kind}]: DID NOT CONVERGE")
failures += 1
continue
worst, where = compare(base, emitted)
if worst is None:
if bound is None:
print(f"{stem} [{kind}]: {where} (documented: {reason})")
else:
print(f"{stem} [{kind}]: {where}")
failures += 1
elif bound is not None and worst <= bound:
note = f" (documented: {reason})" if reason else ""
print(f"{stem} [{kind}]: max deviation {worst:.3e} at {where} ok{note}")
else:
print(f"{stem} [{kind}]: max deviation {worst:.3e} at {where} FAIL")
failures += 1
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())