Reading IIDM Input
IIDM is the native serialization of powsybl-core's network model — .xiidm
files — and the second of the two formats published remedial-action test
material comes in, the other being UCTE-DEF.
#![allow(unused)] fn main() { let net = gridoxide::iidm::read("nordic32.xiidm")?; let report = gridoxide::run_power_flow( net.buses.clone(), &net.lines, &net.transformers, &net.shunts, PowerFlowOptions::default(), ); }
Behind the iidm feature. It costs one crate (quick-xml, already in the tree
as a transitive dependency of the CGMES decoder) and no system library, so like
ucte and opf it is built and tested in CI.
Version tolerance is the design constraint
The IIDM fixtures available for testing span eleven schema versions, 1_0
through 1_16, and pypowsybl 1.16 writes 1_17. The model moved underneath
them. Limits are the clearest case:
<!-- older --> <!-- newer -->
<iidm:currentLimits1 <iidm:operationalLimitsGroup1 id="DEFAULT">
permanentLimit="721.7"/> <iidm:currentLimits permanentLimit="5000.0"/>
</iidm:operationalLimitsGroup1>
and danglingLine became boundaryLine along the way.
So the importer accepts any 1_* namespace, understands both spellings, and
skips elements it does not recognise rather than failing. A parser pinned to
one version reads a fifth of the available material and breaks against the next
powsybl release.
What it does not do is skip silently. Every unrecognised element type is counted and named:
note: skipped 4 `hvdcLine` element(s)
note: skipped 2 `slackTerminal` element(s)
note: 1 branch(es) omitted as disconnected
One topology, two spellings
IIDM voltage levels come in two kinds, and this importer folds them into one
representation. A busBreakerTopology names its buses; a nodeBreakerTopology
numbers its nodes and connects them with switches. Both become nodes of a single
NodeBreakerTopology, and the bus view is whatever
[bus_view] makes of it — for a bus-breaker file with no switches, exactly the
buses the file declared; for a node-breaker file, the connected components of
its closed switches.
That is not merely tidy. It is why a topological remedial action will be
expressible on an IIDM network at all: the switches survive import as
first-class objects instead of being resolved away, so RetentionPolicy picks
the view rather than the parser fixing it.
#![allow(unused)] fn main() { let options = IidmOptions { retention: RetentionPolicy::RetainAll, ..Default::default() }; let net = gridoxide::iidm::read_with(path, &options)?; net.topology.switches.len(); // 17 on the node-breaker fixture net.view.n_buses(); // more than under MergeAll — the switches now split buses }
Boundary nodes
A dangling (boundary) line runs from a real bus to a boundary node — what UCTE
calls an X-node. gridoxide keeps that boundary as a real bus rather than
collapsing the half-line into an injection, for two reasons: it is what the
UCTE importer does with an X-node, so the two agree structurally on
the same network; and it makes tie lines fall out for free. The two halves of a
tie line share a pairingKey, so they land on the same boundary bus with no
pairing logic at all — which covers both the newer form (a tieLine naming two
danglingLines) and the older one (both halves inline as _1/_2 attributes).
A boundary node belongs to no declared voltage level and so has no nominalV.
It adopts the voltage of whatever it attaches to; without that the per-unit base
would default to 1 V and every quantity through the boundary would be nonsense.
How far it agrees
Two gates, and the first is the stronger.
Cross-format. Three fixtures are pypowsybl exports of .uct files the
UCTE suite already checks against
pypowsybl. gridoxide reads the same network through two parsers sharing no code
and the answers must agree. On the twelve-node case, with and without the phase
shifter at tap 16 of 16, they are bit-identical — zero deviation across all
16 branches. On the case with 400/225 transformers and X-nodes the worst
deviation is 6e-12 MW, once the seven boundary half-lines whose terminals
the two formats declare in opposite order are accounted for.
That last point is a labelling difference, not a physical one: a UCTE ##L
record may name the X-node first where the IIDM boundary line names the real bus
first, which flips the reported sign of a flow without changing anything.
Against pypowsybl, on fixtures written as IIDM in the first place — which is
what covers node-breaker topology, temporary limits and the older spellings,
none of which UCTE can express. nordic32 (52 buses, 80 branches) agrees to
1.2e-3 MVar on quantities of several hundred MVar. The residual is the same
known model difference the UCTE page quantifies: IIDM places a transformer's
magnetizing shunt entirely on side 2 while network::branch_calc_param splits
it equally between both ends.
Not modelled
-
HVDC.
hvdcLine,vscConverterStationandlccConverterStationare counted and then skipped with a note. gridoxide has a real DC-side network (crates/gridoxide-core/src/dc.rs) reachable from CGMES, and wiring IIDM into it is its own piece of work — one no fixture in the tree calls for, since none contains an HVDC element.This page used to say the same thing while the importer did something else: a
vscConverterStationshared thegeneratorarm, and since a converter station states notargetPit became a generator producing zero, silently losing the transfer. Counting it is what the page claimed and what it now does. -
Extensions.
slackTerminal,referenceTerminal,mergedXnode,busbarSectionPosition, virtual hubs — all named innotesand skipped. NotablyslackTerminalis where powsybl records a chosen slack, so gridoxide currently picks its own (largest generation, preferring a voltage-regulating bus, ties broken on the bus label) and says so innotes. -
A generator's reactive capability as a curve.
reactiveCapabilityCurvestatesminQ/maxQas a function of P;types::Buscarries one pair. The importer takes the curve's envelope and reports how many generators it did that for. It is an outer approximation — never forbidding an output the machine can produce, possibly permitting one it cannot at its dispatched P — and it replaces no limit at all, which was the same kind of approximation and a worse one. AminMaxReactiveLimitspair is read exactly. -
Asymmetric line shunts. IIDM states
g1/b1andg2/b2separately;Linecarries one total that the π-model splits equally. Every line in every vendored fixture is symmetric, and any that is not is counted innotesrather than silently averaged.
Individual generators and loads stay addressable
The importer folds every injection into its bus's net p_spec/q_spec, which is all a power flow
needs and is not reversible afterwards. IidmImport::injections keeps the correspondence back:
each generator and load by its own IIDM id, the bus it sits on, and the injection the file states.
That is what lets something address an individual machine rather than a bus. A Dynawo dynamic
model attaches to a generator by staticId, and a dynamic study needs each machine's own terminal
power rather than its bus's total — see Reading Dynamic Data.