reduction — model order, and what a time-domain run means
G2ELin was built to model every element in full: the network’s electromagnetic transients, a machine’s stator flux, a converter’s filter and inner loops. That is the right model for studying converter-network interaction, and the wrong one for a 118-bus electromechanical study — not because it is inaccurate, but because those fast dynamics are what makes a model stiff, and a stiff model of a large network is a model nobody waits for.
Model order reduction is the switch between the two. It is the same network, the same operating point and the same equations; what changes is which states are still integrated.
The one idea
Every element’s model is built as a DAE: differential equations for its states, algebraic constraints, outputs. Reducing the order means moving an equation from the first group to the second:
Nothing else changes. The linearisation
(linearize()) already
eliminates algebraic variables, the interconnection’s closed-loop formula
already resolves the algebraic loops this creates — with a quasi-stationary
network that elimination is the admittance-matrix solve a phasor tool does
— and the nonlinear solver already solves g(x, z, u) = 0 coupled across
the whole network. Even the initialisation is untouched: the operating point
is looked up by symbol, so a state that moves from x to z simply arrives
in a different vector.
Two reductions, not one
Warning
algebraic and frozen are not two words for “drop this state”. Using
one where the other belongs is the most common way a hand-rolled reduced
model goes quietly wrong.
Mode |
Operation |
Valid for |
Means |
|---|---|---|---|
|
|
— |
the state is integrated |
|
|
states faster than the study |
the state responds instantly |
|
|
states slower than the study |
the state does not move |
algebraic is residualization, the singular-perturbation limit: correct for
stator flux, filter currents, network branches, inner control loops.
frozen is truncation: correct for the field flux in the classical machine
model, where E' is held constant.
The field winding is the case that catches people. Its time constant
(T'do) is 5–10 s — a slow state. Setting dψ_fd/dt = 0 gives an
instantaneous field winding, the exact opposite of the classical model.
That is why the 2nd-order level freezes it and every other level keeps it
dynamic.
The catalogue
The unit of control is a state group — one physical approximation, such as “stator flux” or “inner current loop” — not an individual state. A level is a named preset over those groups. Anything a level expresses, explicit group settings can express too; levels exist so the familiar models have their familiar names.
Network
Level |
Meaning |
|---|---|
|
every passive element integrates its own |
|
every passive element is algebraic — the phasor network |
Groups: nodes, lines, loads, shunts, transformers, so a mixed
network (dynamic lines, algebraic loads) is expressible.
network_frequency decides whether the ω·L and ω·C terms follow the
reference frame’s own speed (the default, what an EMT model does) or are
pinned to nominal, which is what phasor tools do.
The catalogue is per unit, not per type
A machine’s AVR, stabiliser and governor, and a converter’s power-control
law, are chosen per unit, and they decide which state groups that unit even
has: a machine with the Kundur exciter has an avr group of two states
rather than four, one with no governor has no governor group at all, and a
converter running VSM has frequency and flux where droop has
power_filter.
So element(kind) answers about the type — the default models — while
Network.unit_element(der) answers about one unit, which is what every
per-unit caller goes through. The group ids are shared wherever the
physics is, so a saved level or per-group override stays meaningful when a
model is swapped; only the states behind the group change.
Synchronous machine
The controller states (governor, PSS, AVR) stay dynamic in every named level: a reduced-order machine in a stability study keeps its full controls. The totals below are for the default regulators — a different exciter or a missing governor shifts every row by the same amount.
Level |
Machine states kept |
Total |
|---|---|---|
|
order-8 plus the step-up transformer current |
19 |
|
|
17 |
|
stator flux algebraic (Sauer–Pai / Anderson–Fouad) |
15 |
|
6th order without the second q-axis damper |
14 |
|
two-axis ( |
13 |
|
flux decay / one-axis |
12 |
|
classical: swing only, field flux frozen |
11 |
Grid-forming converter
Level |
Dropped |
Total |
|---|---|---|
|
— |
15 |
|
transformer current |
13 |
|
+ LC filter |
9 |
|
+ inner current loop |
7 |
|
+ cascaded voltage loop |
5 |
|
+ DC link → droop law and power filters only |
3 |
Grid-following converter
Level |
Dropped |
Total |
|---|---|---|
|
— |
14 |
|
transformer current |
12 |
|
+ LCL filter |
8 |
|
+ inner current loop |
6 |
|
+ DC link |
4 |
|
+ outer loops → a current source behind its PLL |
2 |
θ, θ_pll and the swing equation can never be removed: they define the
frames everything else is written in.
What the choice makes the simulation
The web interface calls this the model class, and derives it rather than asking for it:
Class |
When |
A time-domain run is |
|---|---|---|
EMT |
network dynamic and units keep their fast electrical states |
an electromagnetic-transient simulation |
RMS |
neither does |
an electromechanical (phasor) simulation |
Mixed |
one but not the other |
valid, and neither of the above |
This is why the interface’s time-domain page is no longer called “EMT simulation”: what it produces depends on the model order, and both are first-class.
Pairing rules
A reduced model is not a pick-and-mix. Three combinations are buildable but
physically inconsistent, and g2elin_core.network.validation warns
about each:
Quasi-stationary network + a unit that still integrates its transformer current. The unit keeps an electromagnetic transient the grid it feeds no longer has.
Quasi-stationary network + dynamic stator flux (or a dynamic converter filter). The standard pairing drops both together; keeping only the machine’s produces modes with no physical counterpart.
Quasi-stationary network +
nodes_share_first_line_b. A quasi-stationary bus equation is the power flow’s bus equation, so a shunt susceptance the power flow never saw puts the two out of agreement. Measured on WSCC-9: bus voltages settle 0.03 pu away from the power flow’s and the units start ~3000× further from equilibrium. A dynamic network hides this as a transient that decays in microseconds; a quasi-stationary one cannot.
They are warnings, not errors. Asking for a mixed-timescale model on purpose — to find out what one element’s fast dynamics contribute — is a legitimate thing to do, and none of these makes the model unsolvable.
The same categories classify modes
Every state group also declares what kind of phenomenon it is, and
g2elin_core.modal.classify uses that to label each mode by which kind
of state does most of the participating in it. The eigenvalue map draws one
marker shape per kind.
Kind |
What is in it |
|---|---|
Synchronisation |
machine swing, grid-forming droop angle and power filters, grid-following PLL |
Control |
AVR, exciter, PSS, governor, converter voltage and current loops |
Unit electrical |
stator and rotor flux, damper windings, filters, DC link, step-up transformer current |
Network |
bus voltages and branch currents |
Mixed |
no kind holds half of it — often the interesting ones |
Reference angle |
the model’s own free angles: coordinates, not dynamics |
The split is by mechanism, not by which box the state sits in. A converter has no rotor, but its droop law and a machine’s swing equation do the same job, so they share a kind and “which are the synchronisation modes?” has one answer across a mixed fleet.
Note
Synchronisation wins on a much smaller share than the others — 20% rather than 50%. That is the field’s own definition: a mode is electromechanical when the rotor states participate significantly, not when they participate most. Every machine’s AVR and exciter take part in its local mode too, and there are three times as many of those states, so a plain largest-share rule hands rotor modes to the controllers riding on them. Measured on WSCC-9: the 1.63 Hz local mode is 32% rotor and 58% control.
The same rule correctly leaves the 0.66 and 0.76 Hz modes as control — those are 1–2% rotor, field-flux and exciter modes that merely happen to sit in the frequency band where an inter-area mode would be. Frequency alone would misclassify them; participation does not.
Is it safe? The adequacy check
The usual answer to “can I use a reduced model here?” is “it depends on your
case”, which is no help in front of a specific case. g2elin_core.modal.adequacy
gives a specific answer, and it can, because the tool has both halves: the
full-order model, and participation factors.
It does two things:
The screen. Linearise at full order. For every state the reduction would remove, measure its participation in the modes inside the band of interest. A state that only ever participates in modes far outside that band can go — that is the timescale separation singular perturbation needs, verified on this network rather than assumed. A state carrying weight in a slow mode is one whose removal will move that mode.
The measurement. Build the reduced model too and compare the spectra mode by mode in the band, pairing them by proximity. So the report says both “this looked risky” and “here is what it actually cost”.
A mode that disappears is not automatically a loss. Dropping the damper
windings deletes their own time constants; that is the reduction doing what
it was asked. So each vanished mode is weighed by how much of it the removed
states accounted for: more than half, and it was theirs to take
(expected_loss); less, and the reduction has taken out something the
surviving states were part of. Without that distinction every real reduction
looked unsafe — on WSCC-9’s classical model, 9 modes flagged as lost where
only 3 genuinely were — and a verdict that always says no tells you nothing.
Verdicts: safe (every mode reproduced), check (modes move — fine for
screening, not for controller design), unsafe (a mode the surviving states
were part of has no counterpart at all).
On WSCC-9 the check accepts a quasi-stationary network with 6th-order
machines and rejects the classical model, naming ψ_2q at 58 % participation
in a 4.7 Hz mode as the reason. That is a real property of that network, not
a rule of thumb.
Where the settings live
On the Network, not on the analysis
request. The level is part of what a saved case is; it travels with an
exported network; and the API’s model cache is keyed on the network’s own
JSON, so changing a level invalidates every cached model for free.
from g2elin_core.network.schema import ModelOptions
network.models = ModelOptions(
network_level="quasi_stationary",
sm_level="order6",
gfm_level="droop",
sm_states={"avr": "frozen"}, # per-group override, on top of the level
)
network.der_units[2].level = "order2" # and per unit, on top of that
Performance
Reduction removes states, but what decides how long a run takes is the stiffness. On WSCC-9, a quasi-stationary network with 6th-order machines goes from 88 states to 46 — and from a fastest mode at 8.2 × 10⁶ rad/s to 6.3 × 10³, a factor of 1300. That ratio is the number to watch, and it is why the adequacy report prints it.
Note
Reduction moves states into the algebraic vector, so the coupled Newton
solve gets larger, not smaller. It is only a win because that solve is
sparse: the Jacobian of a 118-bus model is 0.055 % non-zero, and solving it
sparsely rather than densely took one right-hand-side evaluation there from
15.9 s to 0.037 s. See g2elin_core.timedomain.emt.
Further reading
The framework is singular perturbation theory; see Kundur §5.3 and §13 for the machine-side pairing rules, IEEE Std 1110 for the model-order naming, and Milano et al., Foundations and Challenges of Low-Inertia Systems (PSCC 2018) for why the quasi-stationary assumption is under pressure in converter-dominated grids. The adequacy check exists because that last question — when is RMS not good enough — is usually answered only after the fact, by an EMT run that disagrees.
The catalogue of model-order reductions: which states each element type
can give up, what the named levels are, and how a choice resolves to the
per-state modes g2elin_core.components.base.apply_reduction() wants.
This module holds no physics. It is a description of the choices, shared
by the symbolic model builders (components/*.py), the schema that
persists a choice (network/schema.py), the validation that rejects an
inconsistent one (network/validation.py), and the API that shows the
choices to the web UI. Keeping it in one place is what stops the level
names, the state groupings and the UI’s checkboxes drifting apart.
Granularity. The unit of control is a state group, not an individual state: the d- and q-axis halves of a stator flux, or the two integrators of a current PI, are one physical approximation each and there is no meaningful model in which one is kept and the other dropped. Groups are also what make the named levels (“6th order”, “droop only”) expressible as a handful of settings rather than a list of nineteen.
Named levels vs. free choice. A level is nothing more than a preset
mapping of groups to modes; anything a level can express, a set of explicit
group overrides can express too, and overrides are applied on top of a
level. So the UI can offer both the familiar names and the underlying
per-group dynamic | algebraic | frozen control without the two being
different mechanisms.
See g2elin_core.components.base.apply_reduction() for what the three
modes mean and why algebraic and frozen are not interchangeable.
- class g2elin_core.reduction.ElementModel(kind: 'str', label: 'str', groups: 'tuple[StateGroup, ...]', levels: 'dict[str, dict[str, str]]', level_labels: 'dict[str, str]', level_notes: 'dict[str, str]'=<factory>, default_level: 'str' = 'full')[source]
Bases:
object- Parameters:
- groups: tuple[StateGroup, ...]
- modes_by_group(level=None, overrides=None)[source]
Every group’s mode, after applying
levelthenoverrides.
- unmet_requirements(by_group)[source]
(group, groups it still needs)for every group made algebraic without what it depends on – seeStateGroup.requires.Checked up front rather than left to surface as a singular Jacobian deep inside the linearisation, which says nothing about which setting caused it.
- class g2elin_core.reduction.StateGroup(id, label, symbols, states, allowed=('dynamic', 'algebraic'), default='dynamic', note='', category='unit_electrical', requires=())[source]
Bases:
objectOne physical approximation a user can switch on or off.
- Parameters:
- g2elin_core.reduction.element(kind, *, exciter=None, pss=None, governor=None, controller=None)[source]
The catalogue for one element type.
exciter,pssandgovernorpick a synchronous machine’s regulators, andcontrollera grid-forming converter’s power-control law; their state groups depend on the models chosen. Callers that hold a unit pass them; callers asking about the element type leave them out and get the defaults.
- g2elin_core.reduction.resolve_modes(kind, level=None, overrides=None)[source]
{symbol name: mode}for one element type, ready forg2elin_core.components.base.apply_reduction().
- g2elin_core.reduction.describe(kind)[source]
The catalogue for one element type as plain JSON-able data – what the API hands the web UI to build its pickers from, so the UI never hard-codes a level name or a state group.
A machine also carries its regulator choices, each with the state group it contributes, so the UI can redraw the AVR and PSS rows when one is swapped.
- g2elin_core.reduction.category_of_state(state_name)[source]
Which of
CATEGORIESone assembled state belongs to.
- g2elin_core.reduction.group_of_state(state_name)[source]
The reduction group one assembled state belongs to, as
"<kind>.<group>", or None when it belongs to none (a reference frame or an infinite bus).
- g2elin_core.reduction.split_state_name(state_name)[source]
"psi_d_{SM_1}"->("psi_d", "SM_1").The split is on the last
_{: a display name can itself contain braces, as"v_{g_d}_{Nd_3}"does.
- g2elin_core.reduction.sm_element(exciter='g2elin', pss='g2elin', governor='g2elin')[source]
The catalogue for a machine carrying one particular set of regulators.
Only the
governor,pssandavrgroups differ: the group ids are the same whichever model is chosen, so a level, an override or a saved network stays meaningful when a machine’s exciter is swapped – only the symbols behind the group change. A regulator set tononecontributes no group.- Parameters:
- Return type:
- g2elin_core.reduction.gfm_element(controller='droop')[source]
The catalogue for a converter running one particular control law.
Only the outer-loop groups differ; the filter, DC link and the two cascaded loops are the same whichever law is running, which is why the named levels below mean the same thing for all of them.
- Parameters:
controller (str)
- Return type:
Is this model-order reduction safe for this network?
The usual answer is “it depends on your case”, which is useless in front of a specific case. This module gives a specific answer, and it can, because the tool already has both halves: the full-order model, and participation factors.
The method. Linearize the network at full order once. For every state the chosen reduction would remove, look at how much it participates in the modes that survive – specifically, in the slow modes the reduced model is supposed to reproduce. A state that is only ever involved in modes far outside the band of interest can be removed without consequence: that is the timescale separation singular-perturbation theory needs, verified on the actual case rather than assumed. A state carrying real participation in a slow mode is a state whose removal will move that mode, and the reduction is not safe.
And then it checks. The participation screen predicts; the second half of the report measures. It builds the reduced model too and compares the two spectra mode by mode in the band of interest, so the report says both “this looked risky” and “here is what it actually cost you”.
The band is a parameter because it is the study that defines it: electromechanical oscillations live below ~2-3 Hz, converter-driven interactions reach tens of Hz, and a sub-synchronous resonance study cares about a band a classical stability study would happily discard.
- class g2elin_core.modal.adequacy.StateRisk(state, group, mode_hz, mode_damping_pct, participation)[source]
Bases:
objectOne removed state that carries weight in a mode worth keeping.
- class g2elin_core.modal.adequacy.ModePair(full_hz, full_damping_pct, reduced_hz, reduced_damping_pct, removed_share=0.0)[source]
Bases:
objectOne mode of the full model and its match in the reduced one.
- Parameters:
- property expected_loss: bool
An unmatched mode that was the removed states’ own.
Dropping the damper windings deletes their time constants; that is the reduction working, not failing. Counting those as lost dynamics made every real reduction look unsafe, which would have made the verdict worthless.
- class g2elin_core.modal.adequacy.AdequacyReport(band_hz: 'float', n_states_full: 'int', n_states_reduced: 'int', removed_states: 'list[str]', risks: 'list[StateRisk]', modes: 'list[ModePair]', max_d_hz: 'float', max_d_damping_pct: 'float', unmatched: 'int', stiffness_full: 'float', stiffness_reduced: 'float', verdict: 'str', notes: 'list[str]' = <factory>)[source]
Bases:
object- Parameters: