Reading Dynamic Data

Three readers, all producing the same internal representation. Nothing downstream of them knows which format a case arrived in.

gridoxide JSON

The native format, and the one the other two target. A dynamics section alongside the ordinary buses and lines:

{
  "buses": [ … ], "lines": [ … ],
  "dynamics": {
    "s_base": 100.0, "f_nom": 50.0,
    "fixed_buses": [2],
    "units": [
      { "id": "G1", "bus": 0,
        "machine": { "model": "gen_round", "h": 5.0, "d": 1.0, "ra": 0.003,
                     "xd": 1.8, "xq": 1.7, "xdp": 0.30, "xqp": 0.55,
                     "xdpp": 0.22, "xqpp": 0.25, "xl": 0.15,
                     "td0p": 8.0, "tq0p": 0.4, "td0pp": 0.03, "tq0pp": 0.05,
                     "mbase": 100.0 },
        "avr": { "model": "sexs", "k": 200.0, "ta": 0.1, "tb": 1.0, "te": 0.05 },
        "gov": { "model": "tgov1", "r": 0.05, "t1": 0.5, "t2": 1.0, "t3": 5.0, "dt": 0.0 } }
    ],
    "loads": [ { "id": "L1", "bus": 1, "zip": [0.3, 0.3, 0.4], "cutoff": 0.5 } ],
    "events": [
      { "kind": "bus_fault",   "t": 1.0, "bus": 1 },
      { "kind": "clear_fault", "t": 1.1, "bus": 1 },
      { "kind": "branch_trip", "t": 1.1, "branch": 1 }
    ],
    "relays": [
      { "id": "uv1", "watch": "bus_voltage", "bus": 1,
        "when": "below", "threshold": 0.7, "delay": 0.2,
        "action": { "kind": "branch_trip", "branch": 1 } }
    ]
  }
}

The model parameter blocks are the models' own parameter structs. That is deliberate: the file format cannot drift away from what the models take, because a renamed field is a missing-field parse error rather than a silently defaulted zero.

A relay's action and a scheduled event are spelled the same way — one vocabulary for what can happen, whether a time or a threshold decides when. A relay carries no time at all: see Events and Discontinuities.

speed_voltages (default false) chooses the machine formulation for the whole case — see The Model Library.

fixed_buses names buses held at constant voltage for the whole run — an infinite bus, represented exactly rather than approximated by a large inertia or a small source impedance. It is optional: a system with no fixed bus is perfectly well posed, since every machine's rotor angle is an absolute state and the whole system's frequency is free to move.

A device may state its own p and q; omitting both makes it take whatever its bus's solved injection has left. See Initialization for why that matters.

PSS/E .dyr

The format most published dynamic cases ship in, and the one ANDES reads — so the same file can drive both gridoxide and a reference, which removes a class of "the two tools were given different data" disagreement before it starts.

Free-format records terminated by /, spanning any number of lines:

     1 'GENROU' '1 '  8.0  0.03  0.4  0.05  5.0  1.0
                      1.8  1.70  0.30  0.55  0.22  0.15  0.1  0.3  /
     1 'SEXS'   '1 '  0.10  1.00  200.0  0.05  -5.0  5.0  /

A .dyr cannot stand alone. Two things every machine model needs are simply not in it — the MVA rating and the armature resistance, which are the .raw's MBASE and ZSORCE — and neither is the bus numbering. All three are demanded from the caller rather than invented, and the error message says where they live.

Two parsing details matter. A record ends at the /, not at the newline, so a three-line GENROU is one record with fourteen parameters rather than three truncated ones. And a leading / is a comment only when no record is open — mid-record the same character is the terminator, and files do put it on a line of its own.

SEXS's first field is the ratio T_a/T_b, not a time constant. Misreading it gives a plausible exciter an order of magnitude too fast.

Dynawo .dyd / .par / .crv

Dynawo is MPL-2.0, RTE-maintained, ships validated cases, and reads IIDM — which gridoxide's IIDM importer already handles. A .dyd lists blackBoxModel entries naming a Modelica lib, a parId into a .par, and optionally a staticId tying it to an IIDM element; a .par holds named parameter sets; a .crv says what to record.

Because a .par is addressed by parameter name, the only real risk is looking up a name no file uses — and a hand-written fixture would agree with whatever the reader happened to expect. So the test fixtures are copied verbatim from Dynawo's own repository, and the gates assert against the file's own literal values.

GeneratorSynchronousFourWindings* maps onto the sixth-order machine parameter for parameter, with generator_SNom as the machine base. ...ProportionalRegulations maps onto the proportional regulator and governor exactly. GeneratorSynchronousThreeWindings* maps onto the fifth-order salient-pole machine — its parameter set carries no XpqPu and no Tpq0, so the reader keys on the library name rather than on which parameters happen to be present, which is the difference between reading a model and guessing one from its data.

A whole case, in one call

iidm::dynamics::load_case reads both halves together — the IIDM network through gridoxide's IIDM importer, the models through the reader above — and returns a system ready to integrate. The staticId on each blackBoxModel is the correspondence between them. The joining itself is dyd::case_from_network, which takes the static network as a dyd::StaticNetwork and so does not care which format it came from.

Each machine's own terminal power is recovered exactly rather than apportioned. A power flow produces a bus's total; the IIDM states every load's p0 and q0, and those are precisely what went into that bus's specification, so

machine's injection = bus's solved injection − the loads the file states there

holds for both active and reactive power, at a PV bus as much as a PQ one. That closes for a Dynawo case the one hazard no downstream gate can catch — a wrong device/load split is self-consistent and therefore silent. Two machines on one bus is the case it cannot resolve: their share of the bus's solved reactive power is genuinely not in the file, only the total is, so it is refused by name.

Tap-changing load models, and anything else attached to a static element that is not a generator, are treated as static and named — the network is right, but their dynamics are not there.

One conversion is inferred rather than read, and is flagged in the source: governor_KGover is a gain on the machine's own governor_PNom, while the proportional governor wants one on the network base, so the reader applies k = KGover · PNom / s_base.

Limits are carried through

SEXS's EMIN/EMAX, TGOV1's VMIN/VMAX, Dynawo's voltageRegulator_EfdMinPu/MaxPu and its governor_PMin/PMax all reach the models. Two details are worth knowing: TGOV1 states VMAX before VMIN, so reading them in field order gives a valve limited upside-down that therefore never moves; and Dynawo's power limits are in MW, so they divide by the network base while its field-voltage limits go through unchanged — which is sound because gridoxide's own initialization reproduces Dynawo's efdPu exactly.

Saturation, stated twice and incompatibly

PSS/E gives two points on a curve, S(1.0) and S(1.2). Dynawo gives an exponential characteristic, md/mq/nd/nq. The two are not convertible without committing to a curve shape, and no model here implements saturation. Both readers parse it, neither uses it, and both report a nonzero value — because ignoring it changes answers, and a silent drop is exactly what surfaces later as a small unexplained disagreement with a reference.