CIM / IEC 61970 (CGMES) network + measurement exchange¶
Status (verified 1.95.276): Partly implemented as a Core API with no CLI verb: an RDF/XML importer that takes one document or one per profile (EQ, TP, SSH, SV, DY, boundary), with a node-breaker topology processor, loads of every CGMES class, machines and injections with their reactive limits, voltage control and type, shunts, static var and series compensators, every concrete tap changer, and the SV tap and shunt positions by option; a bridge to
GridModelwith a slack rule and de-energised islands; a controlled solve for remote voltage control, SVCs and regulating shunts; and an exporter. TheGridModelexport, solved-state (SV voltage) import and export, measurement (Meas) mapping toMeasurementSet, operational limits, themd:FullModelheader and theForwardInferenceGuardgating of dated inputs are not implemented. Implemented in:src/GridSim.Core/IO/Cim/(CimImporter.Parseand its.Topology,.Equipment,.Controlsparts,CimNetwork,CimImportOptions;CimGridModelBridge.ToGridModelandBridge;CimControlledPowerFlow;CimExporter.Export). The behaviour is specified in technical ch. 32, CIM/CGMES import and export. Design-to-code name map:
Design name Implemented as namespace GridSim.Core.IO.CgmesGridSim.Core.IO.CimCgmesImporter(ImportFiles,ImportWithDiagnostics)CimImporter.Parse(string rdfXml)orParse(IEnumerable<string> documents); merged by mRID across the documents; diagnostics inCimNetwork.NotesCgmesImportResultCimNetwork(noGridModelorMeasurementSetmember)CgmesExporterCimExporter.Export(CimNetwork), returns one RDF/XML stringCgmesNetworkBuilder,CgmesUnits, theGridModelbuildCimGridModelBridge.ToGridModel(ohm to per-unit throughBaseVoltage)CgmesProfile,CgmesProfileSchema,CgmesModelHeader,CgmesDatasetnot implemented; profiles are passed as separate documents and merged by mRID CimRdfReader,CimTriple,CimObject.csnot implemented as files; a private CimObjectclass insideCimImporteroverSystem.Xml.Linq(XDocument), not a streamingXmlReaderCgmesMapping,CgmesExportOptionsnot implemented SV, SSH, Meas mapping ( SvVoltage,SvPowerFlow,SvTapStep,Analog/Discrete)SSH values are read; SvTapStepandSvShuntCompensatorSectionsbyCimImportOptions;SvVoltage,SvPowerFlowand Meas not implementedShuntCompensator,OperationalLimitSet,RegulatingControlshunts and RegulatingControlread;OperationalLimitSetnot implementedThis is a design record. The current behaviour is documented in technical ch. 32, CIM/CGMES import and export.
This document specifies a CGMES / CIM subset importer and exporter that maps to and
from GridModel (src/GridSim.Core/Model/Model.cs) and MeasurementSet
(src/GridSim.Core/Estimation/MeasurementSet.cs). It is an implementation blueprint,
not code: it fixes the CIM classes in scope, the RDF/XML parsing approach (no external
dependencies), the exact field mapping and per-unit handling, the round-trip equivalence
contract, the test plan, and how every dated input is routed through
ForwardInferenceGuard so the no-forward-inference invariant holds by construction.
1. Purpose and scope¶
GridSim reads MATPOWER .m cases (IO/Matpower), a JSON case folder, and its own
solve/estimate exports. CGMES is the lingua franca of European TSO/EMS toolchains
(ENTSO-E's profiled subset of IEC 61970/61968 CIM). Supporting a CGMES subset lets
GridSim:
- ingest a published or replayed network snapshot (EQ + TP + SV + SSH) as a
GridModeland estimate on it; - ingest CIM
Measanalog values as aMeasurementSet; - export an estimated / solved
GridModelstate back out as CGMES SV (+ a light EQ/TP) for consumption by a standards-conformant tool or an independent validator.
In scope: a steady-state, balanced positive-sequence subset sufficient to reconstruct
a GridModel and a MeasurementSet, and to write back the estimated bus voltages and
branch flows. Out of scope (listed in section 17): three-phase / short-circuit
sequence data, dynamics (DY) profile, diagram layout (DL/GL), CIM difference models,
multi-authority boundary-set stitching, and the 61968 distribution-management packages.
This spec deliberately reuses GridSim's existing import idioms so the CGMES path reads
like the MATPOWER path: a static entry class with a ...WithDiagnostics overload that
returns a model plus human-readable notes (MatpowerParser.ParseWithDiagnostics,
MatpowerParseResult), private DTO/intermediate types, and a post-build
GridModel.Validate() gate that fails loud on structural faults.
2. Governing constraint: no forward inference¶
UnderstandingRestrictions.md draws one line: GridSim runs inference backward or on the
present, never forward on the real grid. The estimator is a filter, not a predictor;
every dated input must lag real time by at least ForwardInferenceGuard.HorizonHours
(169 h = 7 days + 1 h). A CGMES importer is a dated ingestion seam and therefore sits
squarely under invariant #1 ("no ingester accepts a timestamp newer than UtcNow - 169h").
CGMES timestamps live in three places, all of which this design routes through the guard before any model or measurement object is constructed:
md:Model.scenarioTimein every profile'smd:FullModelheader - the instant the snapshot describes. This is the authoritative "as-of" stamp for an EQ/TP/SV/SSH set.md:Model.created- when the file was produced (secondary; checked when present).cim:MeasurementValue.timeStampon each analog value in aMeasfile - the per-sample telemetry stamp, mapped to (and no newer than)MeasurementSet.TakenUtc.
The guard is applied exactly the way MeasurementSet.Load applies it today
(ForwardInferenceGuard.RejectFuture(taken, utcNow, "measurement set")): parse the stamp,
call the guard, and let ForwardInferenceException propagate. There is no code path
that reads a CGMES snapshot without first proving its scenarioTime is historical.
The exporter is the mirror obligation (invariant #2: "no public-data code path produces an
output stamped later than its newest input"). It writes md:Model.scenarioTime equal to
the source state's own as-of stamp (carried on the GridModel / estimate provenance), never
DateTime.UtcNow, and asserts scenarioTime <= newestInputStamp before serialising. CGMES
export is a pure rendering of an already-estimated present/past state; it introduces no new
inference and no forward step.
Because the importer and exporter contain no solve and no dynamics fusion, they do not
touch the Dynamics/ -> Estimation/ review gate (RestrictionReview, invariant #5). They
are pure format translation on guard-checked data. This is stated explicitly so a reviewer
does not have to re-derive it.
3. CGMES background and the profile subset in scope¶
A CGMES dataset is a set of RDF/XML files, one per profile, sharing object identity by
mRID (a UUID carried as rdf:ID / rdf:about). The profiles this design consumes:
| Profile | Code | What it carries | GridSim use |
|---|---|---|---|
| Equipment (core) | EQ | Topology-independent equipment: lines, transformers, machines, loads, shunts, base voltages, terminals | branch/gen/load/shunt structure and impedances |
| Topology | TP | TopologicalNodes and the terminal-to-node association (the bus model) |
the bus set (one Bus per TopologicalNode) |
| Steady-State Hypothesis | SSH | The operating point: load p/q, machine p/q, tap steps, shunt sections, controls | injections and setpoints |
| State Variables | SV | The solved state: SvVoltage (v, angle), SvPowerFlow (p, q), SvTapStep, islands |
Vm/VaDeg, cross-check flows |
| Measurement | Meas | Analog/AnalogValue (+ Discrete) tied to a terminal or PSR |
the MeasurementSet |
CGMES versions differ only in the CIM schema namespace and a few attribute spellings; this
design targets CGMES 2.4.15 (CIM16) as the primary dialect and CGMES 3.0 (CIM100) as
a recognised alternate, selected by the schema namespace URI found in the file (section 5).
The differences are localised to the CgmesProfileSchema constants and never leak into the
mapping logic.
The SSH profile is strongly recommended but optional: when absent, injections fall back to
SV SvPowerFlow terminal flows aggregated per node (section 8.4), and the importer records a
note. EQ + TP are mandatory to build a GridModel; SV is required to populate a solved state
(else buses take flat/nominal Vm); Meas is required only for MeasurementSet import.
4. CIM classes and attributes in scope¶
All names are CIM16 (cim: = http://iec.ch/TC57/2013/CIM-schema-cim16#). Association
targets are given by rdf:resource="#<mRID>". Attributes not listed are ignored on import
(and not written on export) unless section 17 promotes them later.
4.1 Equipment (EQ) - core¶
| Class | Attributes / associations read | Notes |
|---|---|---|
IdentifiedObject |
.mRID (from rdf:ID/rdf:about), .name |
base of every object; name -> Name fields |
BaseVoltage |
.nominalVoltage (kV) |
the per-unit voltage base; referenced by nodes and equipment |
Terminal (ACDCTerminal) |
.ConductingEquipment, .ConnectivityNode, .sequenceNumber |
the incidence glue; end 1/2 by sequenceNumber |
ConnectivityNode |
.ConnectivityNodeContainer |
node-breaker granularity; collapsed via TP to a TopologicalNode |
ACLineSegment (Conductor) |
.r, .x, .bch, .gch (SI: ohm, S), .length |
pi-model line; .gch usually 0 |
PowerTransformer |
(container of ends) | two-winding in scope; N-winding -> note + skip (section 17) |
PowerTransformerEnd |
.r, .x, .g, .b (ohm/S), .ratedU (kV), .ratedS (MVA), .endNumber, .Terminal, .BaseVoltage |
per-end impedance; folded to a single pi branch |
RatioTapChanger |
.stepVoltageIncrement, .neutralStep, .lowStep, .highStep, .neutralU, .TransformerEnd |
-> Branch.TapRatio (with SSH/SV step) |
PhaseTapChangerLinear (and ...Asymmetrical) |
.stepPhaseShiftIncrement (or geometry), .TransformerEnd |
-> Branch.PhaseShiftDeg |
SynchronousMachine (RotatingMachine, RegulatingCondEq) |
.ratedS (MVA), .minQ, .maxQ, .type, .RegulatingControl, .GeneratingUnit |
-> Generator |
GeneratingUnit |
.minOperatingP, .maxOperatingP, .initialP |
-> Generator.Pmin/Pmax |
RegulatingControl |
.mode, .targetValue, .Terminal, .enabled |
mode=voltage -> Generator.Vg, PV/Slack typing |
EnergyConsumer (ConductingEquipment) |
(p/q from SSH) | -> bus Pd/Qd (folded) |
LinearShuntCompensator (ShuntCompensator) |
.bPerSection, .gPerSection (S), .nomU (kV), .maximumSections |
-> bus Gs/Bs (folded, with section count) |
EnergySource / ExternalNetworkInjection |
.governorSCD (ignored); its terminal node -> slack candidate |
grid-boundary injection -> slack (section 8.5) |
CurrentLimit / OperationalLimitSet / OperationalLimitType |
.value (A), .direction |
-> Branch.RateAMva via sqrt(3)*U*I |
4.2 Topology (TP)¶
| Class | Attributes / associations | Notes |
|---|---|---|
TopologicalNode |
.mRID, .name, .BaseVoltage, .ConnectivityNodeContainer |
one Bus per node - the GridSim bus model |
Terminal.TopologicalNode |
association | maps each equipment terminal to its bus |
ConnectivityNode.TopologicalNode |
association (node-breaker inputs) | resolves CN-level terminals to a TN |
4.3 Steady-State Hypothesis (SSH)¶
| Class | Attributes | Maps to |
|---|---|---|
EnergyConsumer |
.p, .q (MW, MVAr) |
bus Pd, Qd (summed per node) |
RotatingMachine / SynchronousMachine |
.p, .q (MW, MVAr; CGMES sign: injection negative) |
Generator.Pg, Qg (sign-normalised) |
TapChanger |
.step |
resolves RatioTapChanger to TapRatio; PhaseTapChanger to PhaseShiftDeg |
ShuntCompensator |
.sections |
shunt susceptance = sections * bPerSection |
RegulatingControl |
.targetValue, .enabled |
Generator.Vg (pu = targetValue/BaseKv), enabled -> typing |
EquivalentInjection |
.p, .q |
boundary injection at a border node |
4.4 State Variables (SV)¶
| Class | Attributes | Maps to |
|---|---|---|
SvVoltage |
.TopologicalNode, .v (kV), .angle (deg) |
bus Vm (= v/BaseKv), VaDeg |
SvPowerFlow |
.Terminal, .p, .q (MW, MVAr) |
injection fallback + flow cross-check |
SvTapStep |
.TapChanger, .position |
tap when SSH absent |
SvShuntCompensatorSections |
.ShuntCompensator, .sections |
shunt when SSH absent |
TopologicalIsland |
.AngleRefTopologicalNode, .TopologicalNodes |
slack reference bus (per island) |
SvStatus |
.ConductingEquipment, .inService |
Branch.InService |
4.5 Measurement (Meas)¶
| Class | Attributes / associations | Maps to |
|---|---|---|
Analog (Measurement) |
.measurementType, .unitSymbol, .unitMultiplier, .phases, .Terminal, .PowerSystemResource |
MeasurementKind + location |
AnalogValue (MeasurementValue) |
.value, .Analog, .sensorAccuracy |
Measurement.Value, Sigma |
MeasurementValue |
.timeStamp |
MeasurementSet.TakenUtc (guarded) |
MeasurementValueSource |
.name (e.g. "SCADA", "PMU", "CALCULATED") |
Measurement.Source |
Discrete / DiscreteValue |
.value (switch state) |
optional: Branch.InService (section 11.4) |
5. RDF/XML parsing approach (no external dependencies)¶
CGMES files are RDF/XML but constrained to a flat, striped serialisation: a document is a
sequence of top-level typed resource elements, each with literal-valued property children and
rdf:resource reference children. GridSim does not need a general RDF/OWL reasoner - it
needs a striped-RDF reader. That is implementable on System.Xml alone (already available in
.NET; no NuGet, satisfying the "no external deps" rule and the zero-warning gate).
5.1 Reader¶
Use System.Xml.XmlReader over a FileStream (streaming, low-allocation, matches the
"throttle disk I/O" house style) to produce a flat list of CimTriple(subject, predicate,
object) where:
- subject = the
rdf:ID(prefix_-> mRID) orrdf:about(#mRIDor full URI) of the enclosing typed element; - predicate = the element-qualified name, either
rdf:type(from the element name of the typed resource, e.g.cim:ACLineSegment) or a property QName (e.g.cim:ACLineSegment.r); - object = a literal (element text) for value properties, or a resource mRID (from the
child element's
rdf:resource) for associations.
Enumeration values arrive as rdf:resource="http://.../CIM-schema-cim16#RegulatingControlModeKind.voltage";
the reader keeps the fragment after # (RegulatingControlModeKind.voltage) as the object.
5.2 Graph¶
Fold the triples into CimObject records keyed by mRID:
CimObject:
string Mrid
string CimType // e.g. "ACLineSegment" (namespace stripped)
IReadOnlyDictionary<string,string> Literals // predicate localName -> text
IReadOnlyDictionary<string,string> References // predicate localName -> target mRID
ILookup or multimap for repeated predicates (e.g. TopologicalIsland.TopologicalNodes)
predicate localName is the tail after the class-qualified dot: ACLineSegment.r -> r,
Terminal.TopologicalNode -> TopologicalNode. Because CIM property names are unique within a
class, storing by localName is safe and keeps the mapping code terse. Repeated associations
(one-to-many, e.g. TopologicalIsland.TopologicalNodes) go into a separate multimap.
5.3 Dataset¶
CgmesDataset is the parsed multi-profile object set: it merges the CimObjects from every
supplied file by mRID (an object may be defined in EQ and augmented in SSH/SV - CGMES
carries the same mRID across profiles with different property subsets), and records which
CgmesProfile each file declared (from md:Model.profile / the rdf:type mix). The header
records (md:FullModel per file) are collected into CgmesModelHeader values carrying
ScenarioTimeUtc, CreatedUtc, Version, Profile, ModelingAuthoritySet.
5.4 Robustness rules¶
- Namespace-agnostic on the CIM prefix value: bind
cimfrom the document's ownxmlns:cim, and recognise CIM16 vs CIM100 by that URI. Fail loud (InvalidDataException) on an unrecognised CIM schema namespace rather than silently mis-parsing. rdf:IDvsrdf:about: normalise both to a bare mRID (strip a leading_,#, or theurn:uuid:/ full-URI prefix). Identity is by this normalised mRID everywhere.- Invariant culture for every numeric parse (
CultureInfo.InvariantCulture,NumberStyles.Float), mirroringMatpowerParser.ParseNum. - Unknown classes and unknown properties are skipped and counted into the diagnostics notes ("skipped N objects of M unmapped classes"), never fatal - forward-compatible with richer files.
6. Namespace and file layout¶
New code lives in GridSim.Core.IO.Cgmes, mirroring GridSim.Core.IO.Matpower. All types are
internal except the two entry classes and the public result/option records, matching the
existing IO surface.
src/GridSim.Core/IO/Cgmes/
CgmesProfile.cs enum { Eq, Tp, Ssh, Sv, Meas, Unknown } + schema-namespace constants
CimTriple.cs readonly record struct (subject, predicate, object, IsResource)
CimObject.cs parsed resource (mRID, type, literals, refs, multimap)
CgmesModelHeader.cs md:FullModel header (ScenarioTimeUtc, CreatedUtc, Version, Profile, Mas)
CimRdfReader.cs XmlReader -> IEnumerable<CimTriple>; header extraction
CgmesDataset.cs merge triples across files by mRID; expose typed selectors
CgmesUnits.cs SI<->per-unit helpers (Zbase, shunt, tap, rating, angle)
CgmesMapping.cs static maps: measurementType/unitSymbol -> MeasurementKind; enum tables
CgmesImporter.cs static ImportFiles(...) / ImportWithDiagnostics(...) -> CgmesImportResult
CgmesImportResult.cs record (GridModel Model, MeasurementSet? Measurements, IReadOnlyList<string> Notes)
CgmesExporter.cs static Export(GridModel, EstimationResult|PowerFlowResult, options) -> files
CgmesExportOptions.cs record (profiles to write, mRID policy, scenario time source, author set)
Test project: tests/GridSim.Tests/Cgmes/ (section 15).
The CgmesImporter public surface, matching MatpowerParser's idiom:
CgmesImportResult ImportWithDiagnostics(
IReadOnlyList<string> profilePaths, // EQ/TP/SSH/SV/Meas files, any order
DateTime? utcNow = null, // for the guard; defaults to DateTime.UtcNow
double frequencyHz = 50.0)
GridModel Import(IReadOnlyList<string> profilePaths, DateTime? utcNow = null) // .Model of the above
utcNow is threaded exactly like MeasurementSet.Load(path, utcNow) so the guard is testable
with a fixed clock (section 15.5).
7. Intermediate data structures¶
Between the CgmesDataset and the emitted GridModel sits one internal builder that resolves
CIM's node-breaker/associative graph into GridSim's bus-branch arrays. Named to fit the codebase
(...Builder, private nested DTOs like SolveImport):
CgmesNetworkBuilder- orchestrates the passes of section 8; owns the mRID->index maps.NodeRecord- oneTopologicalNode's accumulation:Mrid,Name,BaseKv, runningPd,Qd,Gs,Bs,SvVm,SvVaDeg,IsSlack, incident terminal mRIDs.BranchRecord- one line or transformer reduced to a pi-model:FromNodeMrid,ToNodeMrid,RPu,XPu,BPu,TapRatio,PhaseShiftDeg,RateAMva,Name,IsTransformer,InService.GenRecord- oneSynchronousMachine:NodeMrid,Pg,Qg,Vg,Qmin,Qmax,Pmin,Pmax,MBase,Name,Type.
The builder maps TopologicalNode mRID -> a stable 1-based integer Bus.Id (CGMES has no
integer bus numbers). The mRID<->id crosswalk is retained on CgmesImportResult (via a note and
an optional out-map) so export can round-trip identity (section 13.2). Bus Id assignment is
deterministic: nodes sorted by mRID ordinal, then numbered - so two imports of the same dataset
produce byte-identical models (determinism, section 15.4).
8. Import pipeline (EQ + TP + SSH + SV -> GridModel)¶
Ordered passes over the merged CgmesDataset. Each pass is pure and side-effect-free except for
accumulation into the builder.
8.0 Guard first¶
Extract every CgmesModelHeader.ScenarioTimeUtc (and CreatedUtc when present) and call
ForwardInferenceGuard.AssertHistorical(stamps, utcNow, "cgmes snapshot") before pass 8.1.
A future/recent stamp throws ForwardInferenceException and no model is built. The newest
accepted scenarioTime becomes the dataset's as-of instant, carried onto the result for the
export mirror (section 2) and stored as MeasurementSet.TakenUtc when Meas is imported.
8.1 Base voltages¶
Index BaseVoltage.mRID -> nominalVoltage (kV). This is the per-unit voltage base for every
node and every SI impedance conversion downstream (section 9).
8.2 Nodes (buses)¶
One NodeRecord per TopologicalNode. BaseKv from TopologicalNode.BaseVoltage
(fallback: the node's equipment BaseVoltage, else the container's). Build the
Terminal.mRID -> TopologicalNode.mRID map from TP (resolving ConnectivityNode terminals via
ConnectivityNode.TopologicalNode when the input is node-breaker).
8.3 Loads and shunts (folded onto the bus)¶
MATPOWER/GridSim fold demand and shunts onto the bus (Model.cs remarks). For each
EnergyConsumer, find its terminal's node and add SSH .p/.q to that NodeRecord's
Pd/Qd. For each LinearShuntCompensator, add sections * gPerSection (times nomU^2
scaling to the node base, section 9.4) to Gs and sections * bPerSection to Bs. sections
comes from SSH ShuntCompensator.sections, else SV SvShuntCompensatorSections.sections, else
0 with a note.
8.4 Machines (generators)¶
One GenRecord per SynchronousMachine (and ExternalNetworkInjection/EnergySource treated
as generators for typing). Pg/Qg from SSH with the sign convention normalised (CGMES
RotatingMachine.p is load-positive at the terminal, i.e. generation is negative; GridSim
Generator.Pg is generation-positive - negate on import, section 9.5). Vg from
RegulatingControl.targetValue / BaseKv when mode = voltage and enabled. Qmin/Qmax from
SynchronousMachine.minQ/.maxQ (else +/-Infinity, the GridSim "no limit" default).
Pmin/Pmax from GeneratingUnit.minOperatingP/.maxOperatingP. MBase from ratedS
(else BaseMva).
8.5 Bus typing (Slack / PV / PQ)¶
CIM has no explicit bus type; derive it (this is the one genuinely inferential step, and it is a model-structure inference, not a grid-state one, so it is unrestricted):
- Slack: the
TopologicalIsland.AngleRefTopologicalNode(SV) is the reference bus for its island -BusType.Slack. If SV/island is absent, the node hosting anExternalNetworkInjection/EnergySourceis the slack; if several, the largestratedSwins and the rest become PV (a note records the choice). Multiple islands -> multiple slacks, whichGridModel.Validatetreats as a benign multi-island warning. - PV: a non-slack node with at least one
SynchronousMachinewhoseRegulatingControlis voltage-mode and enabled ->BusType.Pv. - PQ: everything else.
8.6 Branches (lines)¶
One BranchRecord per in-service ACLineSegment. Resolve its two terminals to
FromNodeMrid/ToNodeMrid (terminal sequenceNumber 1 = From, 2 = To). Convert SI to per-unit
against the line's BaseVoltage (section 9.2): RPu, XPu, BPu = bch * Zbase. RateAMva
from the branch's CurrentLimit via sqrt(3) * BaseKv * I_amp / 1000 when present.
8.7 Branches (transformers)¶
One BranchRecord per two-winding PowerTransformer, IsTransformer = true. A CGMES transformer
carries impedance on its PowerTransformerEnds (SI, referred to each end's ratedU). Reduce to a
single pi branch on the network base (section 9.3):
- Series impedance = sum of the two ends'
r + jx, each referred to a common base voltage (choose the From end'sratedU), then per-unitised onBaseMva. TapRatiofrom theRatioTapChanger:1 + (step - neutralStep) * stepVoltageIncrement/100, withstepfrom SSHTapChanger.step(else SVSvTapStep.position). Off-nominal turns ratio also folds the two ends'ratedUmismatch:ratedU_from / ratedU_toscaled to the node base.PhaseShiftDegfrom aPhaseTapChangerLinear:(step - neutralStep) * stepPhaseShiftIncrement.RateAMvafrom the endratedSor aCurrentLimit.
An N-winding (3+) transformer is not reduced in this phase: skip it, emit a diagnostic note,
and (section 17) leave a seam for a star-point auxiliary bus. This matches the "fail loud / note,
never silently corrupt" stance of MatpowerUnitNormalizer.
8.8 Solved state (SV)¶
For each SvVoltage, set the node's SvVm = v / BaseKv (per-unit) and SvVaDeg = angle. These
populate Bus.Vm/Bus.VaDeg (initial/fixed voltages) and, when a solved PowerFlowResult is
requested (an ImportOptions.WithSolvedState flag), a PowerFlowResult assembled through the
same NewtonShell.AssembleResult path EstimationResult.ToPowerFlowResult uses - so SV import
and estimate export share one assembly seam and cannot drift.
8.9 Emit and validate¶
Assemble the GridModel (sealed class - copy every property explicitly, as
MatpowerUnitNormalizer.Normalize does), then run GridModel.Validate() and throw on any
ValidationSeverity.Error (InvalidDataException naming every offender), exactly as
MatpowerParser.ParseWithDiagnostics does. Warnings (multi-island, OLTC) become notes.
9. Unit and per-unit handling¶
GridSim is per-unit on BaseMva with impedances per-unit and power in MW/MVAr
(docs/technical/02-data-model-and-per-unit.md, MatpowerUnitNormalizer). CGMES is SI:
impedances in ohms, susceptance/conductance in siemens, voltage in kV, power in MW/MVAr, angle in
degrees. Conversion is the textbook per-unit reduction, centralised in CgmesUnits.
9.1 Bases¶
BaseMva := dataset system base. CGMES has no single system base; adopt 100 MVA
(GridSim default) unless every ratedS agrees on another value. Record the
choice in a note; it is the invariant divisor for all power/impedance pu.
Zbase(kV) := kV^2 / BaseMva (ohms) // same formula as MatpowerUnitNormalizer
Ybase(kV) := 1 / Zbase(kV) (siemens)
9.2 Lines¶
RPu = r_ohm / Zbase(BaseKv)
XPu = x_ohm / Zbase(BaseKv)
BPu = bch_siemens * Zbase(BaseKv) // susceptance scales by *Zbase (=/Ybase), NOT /Zbase
GPu = gch_siemens * Zbase(BaseKv)
BaseKv is the line's BaseVoltage.nominalVoltage. (Note the MatpowerUnitNormalizer remark:
charging susceptance converts by multiplying Zbase, not dividing - the design follows that.)
9.3 Transformers¶
Refer each end's SI r+jx to a common winding (the From end's ratedU) using the turns-ratio
squared, sum, then per-unitise on BaseMva and the From node BaseKv. The off-nominal tap and
the ratedU_from/ratedU_to ratio combine into Branch.TapRatio (dimensionless), so the pi-model
matches MeasurementModel's transformer stamps (yff = (ys+bc)/tapA^2, yft = -ys/conj(tap)).
9.4 Shunts¶
LinearShuntCompensator susceptance is quoted at nomU. Convert to the node base:
Bs_mvar_at_1pu = sections * bPerSection * nomU^2 (MVAr at 1.0 pu on the node), matching the
Bus.Bs definition ("the MW/MVAr it would draw at 1.0 pu voltage", Model.cs). Likewise Gs.
9.5 Power and sign¶
Load EnergyConsumer.p/q are demand-positive -> add directly to Pd/Qd. Machine
RotatingMachine.p/q follow the CIM terminal convention (flow into the equipment); GridSim
Generator.Pg is generation-out-of-machine positive -> negate. This single sign flip is
unit-tested against a case with a known dispatch (section 15.3).
9.6 Voltage and angle¶
SvVoltage.v (kV) -> Vm = v / BaseKv (pu). SvVoltage.angle is already degrees, matching
Bus.VaDeg and MeasurementKind.VaDeg (the estimator holds angles in radians internally but the
model/measurement surface is degrees - MeasurementModel converts on ingest). No re-referencing
is needed: CGMES angles are already in the island's slack frame
(TopologicalIsland.AngleRefTopologicalNode), which the importer makes the GridSim slack - so
the angle datum is consistent by construction (contrast the Gridradar per-feed offset problem in
docs/pmu-gridradar-design-note.md, which does not arise here).
10. GridModel <-> CGMES field mapping (authoritative table)¶
| GridModel field | CGMES source (import) | CGMES target (export) | Transform |
|---|---|---|---|
GridModel.BaseMva |
adopted (100 or agreed ratedS) |
(not a CIM object; implicit) | section 9.1 |
Bus.Id |
assigned from TopologicalNode.mRID (deterministic) |
mRID via crosswalk | 1-based, mRID-sorted |
Bus.Type |
inferred (section 8.5) | island ref / regulating control | derived |
Bus.BaseKv |
TopologicalNode.BaseVoltage.nominalVoltage |
BaseVoltage.nominalVoltage |
kV, direct |
Bus.Pd / Qd |
sum SSH EnergyConsumer.p/q at node |
one EnergyConsumer per node |
MW/MVAr |
Bus.Gs / Bs |
LinearShuntCompensator (sections) |
LinearShuntCompensator |
section 9.4 |
Bus.Vm |
SvVoltage.v / BaseKv |
SvVoltage.v = Vm * BaseKv |
pu <-> kV |
Bus.VaDeg |
SvVoltage.angle |
SvVoltage.angle |
degrees, direct |
Bus.Name |
TopologicalNode.name |
.name |
text |
Branch.From/To |
terminal sequenceNumber 1/2 -> node ids |
terminals to nodes | id crosswalk |
Branch.R/X |
ACLineSegment.r/x or transformer ends |
.r/.x (SI) |
/ and * Zbase |
Branch.B |
ACLineSegment.bch |
.bch |
* / Zbase |
Branch.TapRatio |
RatioTapChanger + step |
RatioTapChanger + SvTapStep |
section 9.3 |
Branch.PhaseShiftDeg |
PhaseTapChangerLinear + step |
PhaseTapChanger |
degrees |
Branch.RateAMva |
CurrentLimit / end ratedS |
OperationalLimit |
sqrt(3)*U*I |
Branch.Name |
ACLineSegment.name / PowerTransformer.name |
.name |
text |
Branch.IsTransformer |
class is PowerTransformer |
class choice | boolean |
Branch.InService |
SvStatus.inService (else true) |
SvStatus.inService |
boolean |
Generator.Bus |
machine terminal -> node id | terminal to node | id crosswalk |
Generator.Pg/Qg |
SSH machine p/q, negated |
p/q, negated back |
sign flip (9.5) |
Generator.Vg |
RegulatingControl.targetValue/BaseKv |
RegulatingControl.targetValue |
pu <-> kV |
Generator.Qmin/Qmax |
SynchronousMachine.minQ/maxQ |
.minQ/.maxQ |
MVAr |
Generator.Pmin/Pmax |
GeneratingUnit.min/maxOperatingP |
.min/maxOperatingP |
MW |
Generator.MBase |
RotatingMachine.ratedS |
.ratedS |
MVA |
Generator.Name |
SynchronousMachine.name |
.name |
text |
GridModel.Oltcs |
RatioTapChanger regulating -> Oltc |
RatioTapChanger.TapChangerControl |
optional (section 17) |
Fields with no CGMES equivalent (Bus.X/Y layout, Generator.H inertia, DynamicsClass,
Protections, Boundaries) are not written and default sensibly on import, exactly as
SolveImport handles export-absent fields. Generator.H in particular is a DY-profile concern,
out of scope here.
11. Measurement mapping (MeasurementSet <-> Meas profile)¶
11.1 Kind resolution¶
A CIM Analog carries measurementType + unitSymbol + unitMultiplier and is tied to a
Terminal (branch-end quantity) or a PowerSystemResource (bus quantity). Resolve to
MeasurementKind via CgmesMapping:
CIM measurementType |
unitSymbol | Terminal? | MeasurementKind |
Notes |
|---|---|---|---|---|
Voltage / LineToLineVoltage / PhaseVoltage |
V |
on a node/PSR | VmPu |
value/BaseKv -> pu |
Angle / VoltageAngle |
deg |
node/PSR | VaDeg |
already slack-frame |
ThreePhaseActivePower / ActivePower |
W |
PSR (bus) | PInjMw |
net injection |
ThreePhaseReactivePower / ReactivePower |
VAr |
PSR (bus) | QInjMvar |
net injection |
ThreePhaseActivePower / ActivePower |
W |
Terminal (branch) | PFlowMw |
end from terminal seq |
ThreePhaseReactivePower / ReactivePower |
VAr |
Terminal (branch) | QFlowMvar |
end from terminal seq |
Current |
A |
Terminal (branch) | IMagPu |
value/Ibase -> pu |
unitMultiplier (M mega, k kilo) scales the raw value to MW/MVAr/kV before the per-unit or
engineering-unit conversion the Measurement record expects (Measurement values are in
engineering units: pu for voltage/current, MW/MVAr for power, degrees for angle -
Measurement.cs).
11.2 Location resolution¶
- Bus quantity (
PInjMw,QInjMvar,VmPu,VaDeg): resolve theAnalog'sPowerSystemResourceor itsTerminal.TopologicalNodeto aBus.Idand setMeasurement.Bus. - Branch quantity (
PFlowMw,QFlowMvar,IMagPu): resolve theAnalog.Terminalto itsConductingEquipment(the branch) and its two nodes ->Measurement.From,Measurement.To, andMeasurement.Endfrom the terminalsequenceNumber(1 ->BranchEnd.From, 2 ->BranchEnd.To). Parallel circuits are disambiguated withMeasurement.Circuitset to the branchName(or"#k"ordinal), exactly asMeasurementSet.TryResolveBranchexpects.
11.3 Value, sigma, source, time¶
Measurement.Value=AnalogValue.value(scaled byunitMultiplier, per-unitised where the kind is pu).Measurement.Sigma: CGMES rarely ships a per-sample sigma. Priority:Analog.sensorAccuracyif present (interpreted as a 1-sigma in the measurement's unit); else a kind-default sigma table inCgmesMapping(e.g.VmPu0.005 pu,PFlowMw1% of value floored at a small absolute,VaDegper the PMU note). The chosen sigma policy is recorded inMeasurementSet.Note. Sigma must be finite and> 0orMeasurementSet.Validatethrows.Measurement.Source=MeasurementValueSource.namelowercased ("scada","pmu","calculated"), matching the existingMeasurement.Sourceconvention (default"scada").MeasurementValue.timeStamp: the newest timestamp across accepted values becomesMeasurementSet.TakenUtc. All timestamps are guard-checked (section 11.5); a set may not mix ages that straddle the horizon.
11.4 Discrete (optional)¶
A Discrete switch-status measurement can drive Branch.InService. In scope only as an
importer-side note in this phase; wiring it to topology is deferred (section 17).
11.5 Guard on measurement timestamps¶
Before any Measurement is constructed, ForwardInferenceGuard.AssertHistorical is called over
every MeasurementValue.timeStamp. This is the Meas-profile analogue of MeasurementSet.Load's
single-stamp check, and it means the CGMES measurement path is a registered dated ingestion seam
(to be pinned by RestrictionInvariantTests, section 16).
12. Import algorithm summary (pseudocode, not C#)¶
ImportWithDiagnostics(paths, utcNow, frequencyHz):
files = paths.map(CimRdfReader.ReadFile) // streaming XmlReader -> triples + header
dataset = CgmesDataset.Merge(files) // by mRID; classify profiles
# --- GUARD FIRST (no-forward-inference) ---
ForwardInferenceGuard.AssertHistorical(dataset.Headers.ScenarioTimes, utcNow, "cgmes snapshot")
asOf = max(dataset.Headers.ScenarioTimes)
require(dataset.HasProfile(Eq) and dataset.HasProfile(Tp)) // else InvalidDataException
b = new CgmesNetworkBuilder(dataset, frequencyHz)
b.IndexBaseVoltages() # 8.1
b.BuildNodes() # 8.2 -> Bus.Id crosswalk (mRID-sorted, deterministic)
b.FoldLoadsAndShunts() # 8.3
b.BuildGenerators() # 8.4
b.InferBusTypes() # 8.5
b.BuildLines() # 8.6
b.BuildTransformers() # 8.7 (2-winding; note+skip N-winding)
b.ApplySvState() # 8.8 (optional)
model = b.EmitModel() # 8.9 -> GridModel.Validate(); throw on Error
measurements = dataset.HasProfile(Meas)
? ImportMeasurements(dataset, model, b.Crosswalk, utcNow, asOf) # section 11
: null
return CgmesImportResult(model, measurements, b.Notes)
13. Export pipeline (GridModel + estimated state -> CGMES)¶
13.1 What is written¶
A minimal, self-consistent CGMES set sufficient for an independent tool (or GridSim's own importer) to reconstruct the state:
- EQ:
BaseVoltageper distinct kV;TopologicalNode(also serves as the bus in a bus-branch export);Terminals;ACLineSegment/PowerTransformer(+ ends, tap changers);SynchronousMachine;EnergyConsumer;LinearShuntCompensator. - TP:
TopologicalNodes and terminal associations (bus-branch: one TN per bus). - SSH: injections and setpoints (
EnergyConsumer.p/q, machinep/q, tapstep). - SV:
SvVoltage(from the estimatedVm/VaDeg),SvPowerFlowper terminal (from the estimate's recomputed flows viaEstimationResult.ToPowerFlowResult->LineFlow),SvTapStep,TopologicalIslandwith the slack asAngleRefTopologicalNode. - Meas (optional): the
MeasurementSetback out asAnalog/AnalogValue(inverse of section 11) - useful for a validator to check the estimator consumed what it claims.
13.2 Identity round-trip¶
When the source GridModel came from a CGMES import, the mRID<->id crosswalk on
CgmesImportResult is reused so exported objects carry their original mRIDs - a true
round-trip. When the source is a MATPOWER/JSON model with no mRIDs, deterministic UUIDv5-style
mRIDs are minted from a fixed namespace + the bus id / branch endpoints (stable across runs, so
export is reproducible - section 15.4). Minting uses only System.Security.Cryptography (in-box;
no dependency).
13.3 Scenario time (the export mirror of the guard)¶
md:Model.scenarioTime is set from the source state's own as-of instant:
CgmesExportOptions.ScenarioTimeUtc, which defaults to the imported dataset's asOf (section
8.0) or, for an estimate, the MeasurementSet.TakenUtc the estimate was computed from. The
exporter asserts scenarioTime <= newestSourceInputStamp and never substitutes
DateTime.UtcNow. A caller trying to stamp an export in the present/future fails loud
(InvalidOperationException) - the code-level enforcement of invariant #2. md:Model.created
may be UtcNow (it describes file creation, not the modelled instant) but is clearly separated
from scenarioTime.
13.4 Serialisation¶
Write striped RDF/XML with System.Xml.XmlWriter (indented, invariant culture). Deterministic
output ordering: objects sorted by (class, mRID); properties in a fixed schema order. This makes
export byte-for-byte reproducible (the same doctrine as gridsim-solve-export/1:
"byte-for-byte reproducible given a deterministic solve").
14. Round-trip equivalence contract¶
The central correctness property: import then export then import is a fixed point on the
GridModel, and export then import then export is a fixed point on the CGMES bytes.
Define M = model, C = CGMES file set.
import(C) = M0;export(M0, sv0) = C1;import(C1) = M1. RequireM1 == M0under the numeric equivalence of section 14.1 (structure exact, reals to machine epsilon).export(M0) = C1;import(C1) = M1;export(M1) = C2. RequireC2 == C1byte-for-byte (determinism + fixed ordering).
14.1 Numeric equivalence¶
- Integer/enum/string fields (
Bus.Idafter crosswalk,Bus.Type,Branch.IsTransformer, names): exact. - Real fields (
R,X,B,TapRatio,Pd,Vm, ...): equal to within a relative tolerance at the SI<->pu conversion's floating-point round-trip limit. Because pu conversion is a single multiply/divide byZbase, the round tripx -> x/Zbase -> *Zbaserecoversxto a few ULPs; the test tolerance is1e-12relative (the same machine-epsilon class as the solver validation gates), not an engineering tolerance. - Bus voltages carried through SV:
Vmround-trips throughv = Vm*BaseKvthenVm = v/BaseKvto a few ULPs.
15. Validation and test plan¶
Test project tests/GridSim.Tests/Cgmes/, xUnit, matching the existing suite conventions
(ForwardInferenceGuardTests, MatpowerParserTests). All tests run under the zero-warning gate.
15.1 Fixtures¶
- The CGMES conformity mini-cases (ENTSO-E's published "MicroGrid" / "MiniGrid" test
configurations) as the primary real-format fixtures, checked into
tests/.../Cgmes/fixtures/. These ship with a known SV solution - the independent reference (below). - A GridSim-authored round-trip fixture: export
IeeeCases.Case9()/Case14()/Case118()to CGMES, then read back.
15.2 Machine-epsilon cross-checks against an independent reference¶
Two independent references, both giving machine-epsilon (not engineering) agreement:
- Against the CGMES SV file (the format's own published solution): import a MicroGrid
configuration, and for every
TopologicalNodeassert the importedBus.Vm/VaDegequals theSvVoltagevalue to1e-9(SV is stored to full precision; the only arithmetic is the singlev/BaseKvdivide). This proves the parser and unit handling are exact, independent of any GridSim solve. - Against GridSim's own power flow: import the network (EQ+TP+SSH), run
SteadyStateSolver/ Newton-Raphson on the reconstructedGridModel, and assert the solvedVm/VaDegmatches the CGMES SV solution to the solver's convergence tolerance. This proves the impedance/injection mapping is physically correct - the CGMES snapshot and GridSim's physics agree on the same state. (This is a solve on guard-checked historical data, permitted reconstruction, not forward inference.)
The MATPOWER path already cross-validates against published IEEE solutions; the CGMES path adds CGMES SV as a second independent oracle for the same networks where both formats exist.
15.3 Unit and sign tests¶
Zbasereduction: a line with known ohmicr/x/bchand a knownBaseVoltageimports to the hand-computed per-unit values (exact to1e-12).- Shunt:
sections * bPerSection * nomU^2->Bus.Bsat 1.0 pu. - Transformer tap:
step/neutralStep/stepVoltageIncrement->TapRatio; phase shifter ->PhaseShiftDeg. - Machine sign flip: a case with a known dispatch (generation-positive
Pg) imports with the correct sign; export negates it back.
15.4 Determinism¶
- Import the same file set twice (and in shuffled file order, and with shuffled
intra-file object order): assert the two
GridModels are structurally identical and theBus.Idassignment is byte-identical (mRID-sorted numbering). - Export the same model twice: assert byte-for-byte identical CGMES output (fixed ordering + minted-mRID stability).
15.5 No-forward-inference tests (the load-bearing suite)¶
- A CGMES set whose
md:Model.scenarioTimeisUtcNow(orUtcNow - 1h, inside the 169 h horizon) throwsForwardInferenceExceptionon import, with a fixedutcNowinjected - and noGridModelis constructed (assert the exception precedes any partial build). - A set at exactly
UtcNow - 169his on the boundary; atUtcNow - 170hit imports cleanly. - A
Measfile with a value timestamped inside the horizon throws before anyMeasurementis built. - Export refuses a
scenarioTimenewer than the source's newest input stamp (InvalidOperationException), and never emitsDateTime.UtcNowasscenarioTime. - A
RestrictionInvariantTestsaddition exercisesCgmesImporter.ImportandImportMeasurementsas dated seams, pinning them alongside the existing per-seam guard tests (the review log's "Note on invariant #1's marker" lists the seams; CGMES import joins them).
15.6 Round-trip tests¶
The two fixed-point properties of section 14, at 1e-12 relative for reals and exact for
structure/bytes.
15.7 Robustness / negative tests¶
- Unknown CIM schema namespace ->
InvalidDataException(not a mis-parse). - Missing EQ or TP ->
InvalidDataException. - N-winding transformer -> imported model omits it with a diagnostic note (asserted), never a corrupt branch.
- A structurally invalid reconstructed model (dangling terminal, no slack) -> the
GridModel.Validate()error path throws, naming offenders (mirrorsMatpowerParser).
16. How the restriction invariants are honoured¶
Invariant (UnderstandingRestrictions.md) |
How this design satisfies it |
|---|---|
#1 no ingester accepts a stamp newer than UtcNow - 169h |
ForwardInferenceGuard.AssertHistorical runs on every scenarioTime and every MeasurementValue.timeStamp before any model/measurement is built (sections 8.0, 11.5). CGMES import is registered as a dated seam in RestrictionInvariantTests. |
| #2 no output stamped later than its newest input | The exporter sets md:Model.scenarioTime from the source's as-of instant and asserts <= newest input; it never uses UtcNow for scenarioTime (section 13.3). |
| #3 solver forward trajectories are model output only | Not applicable - the importer/exporter run no solver forward step. The optional SV cross-check solve (test 15.2) is a reconstruction on historical data, seeded from and stamped at the guard-permitted scenarioTime. |
| #4 real-time features on permitted-age / replay data | CGMES snapshots are historical by the guard; there is no live CGMES feed path. |
| #5 Dynamics -> Estimation fusion gated by review | Not applicable - format translation fuses no dynamics process model; RestrictionReview.AssertPermitted is untouched. Stated explicitly (section 2) so no re-derivation is needed. |
The importer is, like every other GridSim ingester, structurally incapable of nowcasting: it cannot construct a model from a snapshot it has not first proven is at least a week old.
17. Zero-warning build gate and coding-standard compliance¶
Directory.Build.props enforces TreatWarningsAsErrors=true, EnableNETAnalyzers=true,
AnalysisMode=Recommended, Nullable=enable. The design is written to pass it with no
suppressions:
- No external dependencies: only
System.Xml,System.Text,System.Globalization,System.Security.Cryptography(all in-box). No NuGet package is added, so no new analyser surface and no supply-chain review. - Nullable-clean: intermediate records use non-null defaults; optional CGMES data
(
MeasurementSet?, absent SSH) is modelled with explicit nullable reference types and handled, never null-forgiven without cause (the one!idiom used inMeasurementSet.Savefor a known directory is the pattern to follow if needed). - Culture-invariant every parse/format (CA1305), as
MatpowerParserandSolveImportdo. XmlReader/XmlWriterconfigured withDtdProcessing = Prohibitand no external entity resolution (CA3075 / XXE safety) - a security-review-clean default.- Static entry classes,
internalhelpers,sealedrecords/DTOs - the exact surface shape of the existingIOcode, so no new analyser categories are triggered. - Diagnostics are returned as
IReadOnlyList<string>notes (theMatpowerParseResultpattern), not thrown for soft issues; only structural errors throw.
A green dotnet build with zero warnings is a merge precondition, asserted by the same CI gate
that guards the rest of the tree.
18. Seams to extend and explicit out-of-scope¶
Left as clearly-marked extension points, each behind a note-and-skip today (never a silent approximation):
- N-winding transformers: a star-point auxiliary
Bus+ N pi branches. TheCgmesNetworkBuilder.BuildTransformerspass is the seam. - Full node-breaker topology processing: this phase collapses
ConnectivityNodes via the TP association; a native breaker/switch model (withDiscreteswitch states drivingBranch.InService) is a later phase. - Sequence / short-circuit data (r0/x0): for a future three-phase / unbalanced DSSE (Phase 4). The reader already carries the triples; only the mapping is deferred.
- DY (dynamics) profile:
Generator.Hand machine models. Fusing DY into estimation would cross theDynamics -> Estimationreview gate (invariant #5) and must go throughRestrictionReviewfirst - out of scope for a format translator. - CIM difference models, boundary-set stitching, multi-authority merges, DL/GL diagram layout: recognised, unmapped, counted in notes.
Oltcexport fromRatioTapChanger.TapChangerControl: the import path can already buildGridModel.Oltcs; the export inverse is a small follow-up.
End of specification. Implementation follows this document; deviations are recorded back into it
and, where they touch a dated seam, cross-checked against UnderstandingRestrictions.md and
RestrictionReview.Reviewed (src/GridSim.Core/Diagnostics/RestrictionReview.cs) before merge.