cimhub_opendss Quick Start¶
cimhub_opendss converts power-system models between OpenDSS and the
CIM (Common Information Model) using the cimhub_2026 profile.
This example walks through the three things the package does, using the bundled IEEE 13-bus test feeder:
- Import —
dss_to_cim: read a Master DSS file tree into a CIMFeederModel - Export —
cim_to_dss: write a CIM model back out as OpenDSS files - Roundtrip — solve the original and the roundtripped model and compare
The public API is two functions:
from cimhub_opendss.importer.dss_to_cim import dss_to_cim
from cimhub_opendss.exporter.cim_to_dss import cim_to_dss
Environment setup¶
Two environment variables must be set before any cimgraph/CIM import so the unit system and CIM profile are selected correctly.
We also quiet the converter's per-element INFO/WARNING diagnostics (the IEEE 13
model triggers many benign "load model substituted" notices) so the output below
stays readable. Drop the setLevel(logging.ERROR) line to see the full detail.
import os
os.environ["CIMG_USE_UNITS"] = "TRUE" # enable pint-based CIMUnit wrappers
os.environ["CIMG_CIM_PROFILE"] = "cimhub_2026" # CIM profile used by cimhub_opendss
import logging
import warnings
# Quiet benign init/diagnostic logging so the walkthrough output stays readable:
# - cimgraph emits "empty default graph" notices when a FeederModel is created
# - cimhub_opendss logs per-element "load model substituted" notices for IEEE 13
# Drop these two lines to see the full detail.
logging.getLogger("cimgraph").setLevel(logging.ERROR)
logging.getLogger("cimhub_opendss").setLevel(logging.ERROR)
# tqdm.auto (pulled in transitively) warns when ipywidgets isn't installed; the
# progress bar still works, so silence the cosmetic warning.
warnings.filterwarnings("ignore", message="IProgress not found")
Locate the bundled test model¶
The IEEE 13-bus model ships with the package tests. We resolve its path relative to this notebook so the example runs from any working directory, and write all outputs into a throwaway temp directory.
import tempfile
from pathlib import Path
# notebooks/ lives directly under the package root; tests/ is its sibling.
PKG_ROOT = Path.cwd().parent if Path.cwd().name == "notebooks" else Path.cwd()
MASTER_DSS = PKG_ROOT / "tests" / "test_models" / "IEEE13_CDPSM.dss"
assert MASTER_DSS.exists(), f"test model not found: {MASTER_DSS}"
WORK_DIR = Path(tempfile.mkdtemp(prefix="cimhub_opendss_quickstart_"))
OUTPUT_XML = WORK_DIR / "ieee13.xml"
OUTPUT_DSS_DIR = WORK_DIR / "ieee13_dss"
print(f"model: {MASTER_DSS}")
print(f"outputs: {WORK_DIR}")
model: /home/ande188/CIMHub_2_0/cimhub_opendss/tests/test_models/IEEE13_CDPSM.dss outputs: /tmp/cimhub_opendss_quickstart_6vkeo4n2
1 — Import: DSS → CIM¶
dss_to_cim reads the Master DSS file and every file it Redirects to, and
returns a cimgraph FeederModel. write_xml serialises that model to a CIM
RDF/XML file.
from cimgraph.utils import write_xml
from cimhub_opendss.importer.dss_to_cim import dss_to_cim
feeder = dss_to_cim(MASTER_DSS)
# Namespaces for RDF/XML serialization: the base CIM profile plus the GridAPPS-D
# CIM extension used by cimhub_2026.
namespaces = {
"cim": "http://cim.ucaiug.io/CIM101/draft#",
"gad": "http://gridappsd.org/CIM/extension#",
}
write_xml(feeder, str(OUTPUT_XML), namespaces=namespaces)
print(f"wrote {OUTPUT_XML.name} ({OUTPUT_XML.stat().st_size / 1024:.1f} kB)")
wrote ieee13.xml (240.6 kB)
Inspect the CIM graph¶
A FeederModel.graph is a dict[CIM class → dict[mRID → object]]. Counting the
instances per class is a quick sanity check on what was imported.
for cim_class, instances in sorted(feeder.graph.items(), key=lambda kv: kv[0].__name__):
if instances:
print(f" {cim_class.__name__:<38s} {len(instances):>3d}")
ACLineSegment 11 ACLineSegmentPhase 26 BaseVoltage 6 BatteryUnit 3 ConnectivityNode 21 EnergyConsumer 16 EnergyConsumerPhase 21 EnergySource 1 Feeder 1 LinearShuntCompensator 2 LinearShuntCompensatorPhase 4 LoadBreakSwitch 4 LoadResponseCharacteristic 2 Location 21 NoLoadTest 1 PerLengthPhaseImpedance 7 PhaseImpedanceData 26 PhotoVoltaicUnit 2 PositionPoint 21 PowerElectronicsConnection 3 PowerElectronicsConnectionPhase 8 PowerTransformer 4 PowerTransformerEnd 5 RatioTapChanger 3 ShortCircuitTest 6 SwitchPhase 12 TapChangerControl 3 Terminal 65 TransformerEndInfo 9 TransformerMeshImpedance 4 TransformerTank 4 TransformerTankEnd 9 TransformerTankInfo 4
2 — Export: CIM → DSS¶
cim_to_dss writes one .dss file per component type plus a Master.dss that
wires them together with Redirect statements. It returns a dict of
{filename: [commands]} describing what it wrote.
from cimhub_opendss.exporter.cim_to_dss import cim_to_dss
result = cim_to_dss(feeder, OUTPUT_DSS_DIR)
for filename, commands in sorted(result.items()):
print(f" {filename:<24s} {len(commands):>3d} commands")
Capacitors.dss 2 commands Linecodes.dss 7 commands Lines.dss 11 commands Loads.dss 17 commands PVSystems.dss 2 commands Regcontrols.dss 3 commands Storage.dss 3 commands Switches.dss 4 commands Transformers.dss 6 commands Vsources.dss 2 commands
print((OUTPUT_DSS_DIR / "Master.dss").read_text())
Clear Redirect Vsources.dss Redirect Linecodes.dss Redirect Switches.dss Redirect Lines.dss Redirect Transformers.dss Redirect Regcontrols.dss Redirect Capacitors.dss Redirect Loads.dss Redirect PVSystems.dss Redirect Storage.dss Set VoltageBases=[115.0, 13.2, 4.16, 0.48, 0.208] CalcVoltageBases setkvbase 650z kVll=13.2 setkvbase house kVll=0.2078 Set MaxControlIter=100 Solve
3 — Roundtrip validation¶
The real test of a converter is whether the roundtripped model still solves to the same answer. We solve the original DSS model and the exported one with OpenDSS and compare total circuit power.
Uses the modern OpenDSSDirect
dss.Command("...")interface rather than the deprecateddss.run_command(...).
import opendssdirect as dss
def solve(master_path: Path) -> dict:
"""Compile and solve a DSS model; return a summary of the solution."""
dss.Command("Clear")
dss.Command(f"Compile [{master_path}]")
dss.Command("Set MaxControlIter=100")
dss.Command("Solve")
total_p, total_q = dss.Circuit.TotalPower()
return {
"converged": dss.Solution.Converged(),
"total_p_kw": total_p,
"total_q_kvar": total_q,
"num_buses": dss.Circuit.NumBuses(),
"num_elements": dss.Circuit.NumCktElements(),
}
original = solve(MASTER_DSS)
roundtrip = solve(OUTPUT_DSS_DIR / "Master.dss")
print(f'{"":<14s} {"Original":>12s} {"Roundtrip":>12s}')
print("-" * 44)
for key in original:
print(f"{key:<14s} {str(original[key]):>12s} {str(roundtrip[key]):>12s}")
Original Roundtrip -------------------------------------------- converged True True total_p_kw -3230.490953063305 -3231.1162890489677 total_q_kvar -1711.8266123394076 -1713.808183547551 num_buses 21 21 num_elements 51 49
The total active and reactive power match within the converter's documented
roundtrip tolerance (see the test_ieee13_roundtrip integration test), and both
models report the same bus and element counts — confirming a structurally and
electrically faithful roundtrip.
Next steps¶
- Usage guides: Import, Export, and Roundtrip pages in this documentation
- Schema Reference: the CIM ↔ OpenDSS field mapping, generated from the LinkML schema