network — schema and presets
The typed network model that replaces the MATLAB Y_network/Y_line/
Y_TR/Y_DER numeric-column convention with named, validated
pydantic fields. Every other module in
g2elin_core takes a Network (or the PowerFlowResult computed from one)
as its starting point — see the architecture diagram.
Schema
classDiagram
class Network {
+name: str
+f_hz: float
+sn_mva: float
+buses: list~Bus~
+lines: list~Line~
+transformers: list~Transformer~
+loads: list~Load~
+shunts: list~Shunt~
+der_units: list~DerUnit~
+bus(bus_id) Bus
}
class Bus {
+id: int
+name: str
+vn_kv: float
}
class Line {
+from_bus: int
+to_bus: int
+r_pu: float
+x_pu: float
+b_pu: float
}
class Transformer {
+hv_bus: int
+lv_bus: int
+r_pu: float
+x_pu: float
+sn_mva: float
}
class Load {
+bus: int
+p_mw: float
+q_mvar: float
}
class DerUnit {
+bus: int
+unit_type: UnitType
+bus_type: BusType
+v_set_pu: float
+p_set_mw: float
+controller: GfmController?
}
class BusType {
<<enumeration>>
SLACK
PV
PQ
}
class UnitType {
<<enumeration>>
NONE
INFINITE_BUS
GFM
GFL
SYNCHRONOUS_MACHINE
}
class GfmController {
<<enumeration>>
DROOP
DROOP_FILTERED
DVOC
VSM
MATCHING
}
Network "1" *-- "many" Bus
Network "1" *-- "many" Line
Network "1" *-- "many" Transformer
Network "1" *-- "many" Load
Network "1" *-- "many" DerUnit
DerUnit --> UnitType : unit_type
DerUnit --> BusType : bus_type
DerUnit --> GfmController : controller (GFM only)
Note that load-flow bus type (BusType: slack/PV/PQ) and unit type
(UnitType: infinite bus/GFM/GFL/SM/none) live on DerUnit, not on Bus
itself — matching Y_DER’s columns in the original MATLAB convention. A
plain network node with no DER attached is just a Bus.
Validators (not shown above — see the source) enforce that every bus
reference in a Line/Transformer/Load/DerUnit resolves to a real
Bus, and that exactly one bus is SLACK. These are structural
guarantees the original MATLAB scripts relied on by convention, not
schema — encoding them here means a malformed network fails fast at
construction time rather than deep inside a solver.
Transformers: two roles, one element
Whether a transformer is a unit step-up or a branch is decided by its
LV bus, not by a flag (breakers.unit_transformers /
breakers.branch_transformer_indices):
unit step-up |
branch transformer |
|
|---|---|---|
LV bus |
carries a DER unit |
an ordinary grid bus |
dynamic model |
inside the unit’s own model, as its |
a block of its own, the same RL branch a line uses |
its LV bus |
never appears; it lives inside the unit |
an ordinary node |
|
not modelled (the unit’s own ratio) |
modelled |
opening its breaker |
takes the unit with it |
drops just the branch, as for a line |
A branch transformer used to be absent from the dynamic model entirely while carrying power in the power flow, which left the operating point the model was linearized about not being an equilibrium at the two buses it joined – ~450 pu/s of drift at exactly those buses, with no validation error.
The ratio needs no new component maths. A transformer branch is a series impedance behind an ideal ratio \(a\,e^{j\varphi}\), so the series part sees
which in dq is a \(1/a\) scaling and a rotation by \(-\varphi\) – plain coefficients on the wiring the interconnection already supports, and the same ones in the linear and nonlinear paths. Power is conserved across the ideal part: \(v_{hv} i_{hv} = a v' \cdot i/a = v' i\). Two transformers claiming the same unit’s LV bus are rejected, since that would decide by list order which grid bus the unit injects into.
Bus shunts
A bus’s own dynamic model (components/node.py) is a capacitance to
ground, \(C\,dv/dt = i - j\omega C v\), which makes the two kinds of shunt
behave very differently:
a capacitor bank (
q_mvar < 0) is simply more of that capacitance. It adds no states and no block at all – its current is already in the node equation.a reactor (
q_mvar > 0) cannot be folded in: a negative capacitance would flip the sign of the node’s dynamics, and netting the two against each other would only hold at exactly nominal frequency. It gets the line’s RL branch with its far end wired to an empty sum, which is ground.
Its q_mvar is the nameplate (\(Q = V^2/X\)); the resistance comes from r_pu,
or from \(X/R = 50\) when that is not given. The power flow is handed the P and
Q of that same R-X pair, so both sides of the tool agree on what the device
draws.
Each bus’s capacitance
breakers.node_capacitances gives every bus half the charging of the lines
meeting at it (a pi-model puts half at each end) plus its capacitor banks.
Network.nodes_share_first_line_b restores the MATLAB toolbox’s convention,
where every bus instead borrows the first line’s charging whatever is
connected to it. The ported presets set it, so they still reproduce that
tool’s numbers. Measured, the borrowed value was 0.95x to 1.61x off across
WSCC’s buses and 0.06x to 1.28x across CIGRE’s, and it was the whole reason
the operating point was not an equilibrium: the predicted drift
\(\omega_b |v| (\text{ratio} - 1)\) matched the measured one at every bus.
Giving each bus its own takes WSCC’s drift from 178 to 0.085 pu/s, and makes
EMT faster (7.9 s -> 1.1 s for 50 ms), because the solver no longer chases a
transient that only existed because the starting point was wrong.
A bus whose lines declare no charging at all – distribution-feeder data
routinely omits it – is refused rather than divided by, naming
Network.min_node_b_pu as the way to say what to use instead.
Presets
21 networks in 5 families. The first two are transcribed line-by-line
from a complete MATLAB script, with exact numeric values and source-line
comments tying each field back to the .m file it came from — each
shares one raw topology across every DER-mix variant in its family
(_wscc9_topology()/_cigre_raw_topology()), with a thin per-variant
function supplying just that variant’s own DER rows:
wscc9_3sm()and 7 more DER-mix variants — WSCC 9-bus (Functions/WSCC_raw.m+WSCC/script_WSCC.m’sswitch model.name, onecaseper variant):wscc9_2sm_1gfl,wscc9_1sm_2gfl,wscc9_1sm_1gfm_1gfl,wscc9_1sm_2gfm,wscc9_2sm_1gfm,wscc9_1gfm_2gfl,wscc9_3gfm. The last two have a GFM as the slack — power flow works (pandapower’s slack handling is agnostic to unit type), but modal/EMT raiseNotImplementedError(HTTP 501), since interconnect only wires up an SM/IB slack so far.cigre_islanded_1sm_2gfm_1gfl()and 3 more — CIGRE MV benchmark feeder, islanded (Functions/CIGRE_raw.m+CIGRE/script_CIGRE_Islanded.m’s ownswitch model.name):cigre_islanded_1sm_1gfm_1gfl,cigre_islanded_1sm_3gfm_1gfl,cigre_islanded_2sm_2gfm_2gfl(this last one the only preset with two synchronous machines).cigre_islanded_1sm_1gfm_1gfl()— the smallest network here with one of each major DER type — is the subject of Worked example: CIGRE islanded, 1 SM + 1 GFM + 1 GFL and its companion notebook, CIGRE islanded 1 SM + 1 GFM + 1 GFL — a complete worked example.
Two MATLAB quirks are preserved deliberately rather than silently “fixed,” for fidelity with the original tool’s actual (if arguably buggy) behavior — see the docstrings below for exactly where and why.
The other three are SMIB (single-machine-infinite-bus) presets —
sm_smib(), gfm_smib(), gfl_smib() — and are not a straight port
the way the two above are: Functions/preset_networks.m names exactly
these three cases but its switch body for each is empty, so the
topology/line/load values are real (transcribed from Functions/SMIB_raw.m,
confirmed to share CIGRE_raw.m’s own base-value convention numerically)
while the DER attachment is a documented reconstruction following this
project’s own network_form.m convention. See sm_smib()’s own
docstring for the full account. These are also the first presets to
exercise g2elin_core.components.ib (the infinite-bus component, built
early in this project but never wired into the interconnection until
these presets needed a real slack for it — see interconnect).
Kundur two-area
kundur_two_area() and kundur_two_area_classic() are the first presets
that are not ports of the MATLAB toolbox: two areas of two 900 MVA
machines joined by a weak 220 km double-circuit tie, from Kundur’s Power
System Stability and Control, Example 12.6 — the textbook case for
inter-area oscillations and for what a stabilizer is for. They use this
tool’s own conventions throughout (each bus its own capacitance, each unit
its own transformer), and they are the first to need three of the things
added for them: unit ratings, capacitor banks, and per-bus capacitance.
The book publishes the machine as Xd/Xq/Xl with transient and
subtransient reactances and open-circuit time constants, while
components is written in mutual and leakage inductances
with explicit field and damper windings. kundur_machine_params() converts
between them; tests/test_kundur.py inverts the conversion and checks all
eight published quantities come back.
Checked against the book’s own operating point: the tie carries 400.4 MW (published 400), and the rotor angles come out 9.77°, 27.08° and 37.27° behind G1 against the published 9.7°, 27.0° and 37.2°.
There are two of them because the mode frequencies depend on controls this tool models differently:
inter-area |
local (area 1 / area 2) |
|
|---|---|---|
|
0.71 Hz, +12.9% |
1.23 / 1.26 Hz |
|
0.61 Hz, −2.5% |
1.12 / 1.15 Hz |
Kundur, published |
~0.55 Hz, negative |
~1.1 Hz |
The classic variant reproduces what the example exists to show: without a
stabilizer the inter-area mode is unstable. The governor turned out to
move it most — its 0.5% droop stiffens the mode from 0.61 to 0.71 Hz and adds
one of its own near 0.25 Hz — so classic sets a droop large enough that it
does not respond, the SM model having no governor off switch. The residual
offset from the published frequency is the exciter and stabilizer structure,
which is this tool’s and not the book’s; the tests assert the mode structure
and the sign of the damping rather than frequencies the model cannot be
expected to reproduce exactly.
Topology layout
network.topology.compute_topology_layout() computes a 2D graph layout
(Kamada-Kawai, over the bus graph — lines and transformers as edges) for
the web UI’s Network and Power Flow tabs. A DER unit or load isn’t a
separate geometric node in this schema — a DER sits on its own real Bus
(network_form.m always gives it one), and a load just references an
existing bus id — so the graph to lay out is exactly network.buses +
network.lines/network.transformers; DER/load presence is metadata
decorating an existing node, not a node of its own.
BusNode/TopologyEdge also carry each element’s electrical/control
parameters, not just layout — a Line’s r_pu/x_pu/b_pu/length_km,
a Transformer’s r_pu/x_pu/sn_mva, and (for a DER-hosting bus) the
DerUnit’s own dispatch fields plus, for SM/GFM/GFL, the derived
electrical/control-parameter dict from operating_point.sm_params()/
gfm_params()/gfl_params() — the same values those functions hand to
the linearizer, not a duplicate of the static schema. This needs no power
flow (those three functions are pure functions of network-level static
data — sn_mva/f_hz/the DER’s own bus vn_kv/the first transformer’s
impedance, the same “every unit uses DG#1’s transformer” quirk
components documents), so /topology stays cheap.
The web UI renders this as a click-to-inspect detail panel on each
bus/line/transformer in the diagram.
Validation
pydantic’s own validators on Network guarantee two structural properties
unconditionally (every bus reference resolves, exactly one slack DER).
Every hand-crafted preset in this codebase also satisfies a further
invariant by construction, following network_form.m’s convention: each
DER unit sits on its own private bus, reached from the rest of the network
through exactly one transformer, and that bus is never also a Line
endpoint, a Load’s bus, or another Transformer’s hv_bus. A network
assembled by hand (the web UI’s network editor or drag-and-drop builder)
can easily violate any part of that — e.g. a DER wired with “Wire: Line”
instead of “Wire: Transformer” — and operating_point.compute_operating_point()
plus interconnect.network_assembly.build_blocks_and_wiring() both assume
it unconditionally, previously crashing with a bare KeyError/IndexError
deep inside instead of a clear message when it didn’t hold.
network.validation.validate_network(network) -> list[NetworkIssue] checks
this (plus duplicate bus/DER ids, self-loop lines/transformers, and overall
graph connectivity) in one pass and returns every issue found, not just
the first — each tagged "error" (blocks the affected capability) or
"warning" (e.g. a GFM/GFL slack: power flow still works, modal/EMT
don’t support it yet) and which of powerflow/modal/emt it
affects. compute_operating_point() calls this itself and raises one
combined ValueError listing every "error"-severity issue if any exist
(-> HTTP 422, not a 500) — so this validation runs regardless of whether a
caller checks proactively. The web UI’s network editor also calls
POST /api/network/validate directly (see g2elin_api and the web UI) as a
cheap, no-power-flow-needed pre-flight check, showing every issue in the
editor’s own panel rather than discovering them one crash at a time.
For all of this exercised hands-on — a random_network() builder that
respects every convention above by construction, several deliberately
broken variants showing exactly what validate_network() catches
(including cases these unconditional pydantic validators alone can’t,
like a DER wired to another DER’s own bus), and a 30-seed batch proving
the builder is robust rather than one lucky draw — see
Building a random network from scratch — a tour of Network’s conventions.
Reference
Typed network description for G2ELin.
Replaces the MATLAB toolbox’s numeric-column matrix convention
(Y_network / Y_line / Y_TR / Y_DER) with named, validated
fields. See legacy.py for a converter from the original convention,
used to import existing G2ELin cases.
Per-unit convention: everything is in system per-unit on Network.sn_mva,
matching the original tool (Power_Fl.m / Load_flow.m). Bus nominal
voltages (Bus.vn_kv) are physical, used only for display and for
transformer turns-ratio calculations.
- class g2elin_core.network.schema.BusType(*values)[source]
-
Load-flow bus type, matching
Y_networkcolumn 4 /Y_DERcolumn 4.- SLACK = 'slack'
- PV = 'pv'
- PQ = 'pq'
- class g2elin_core.network.schema.UnitType(*values)[source]
-
DER unit type, matching
Y_DERcolumn 2 (Y_networkcolumn 2 for raw nodes).- NONE = 'none'
- INFINITE_BUS = 'infinite_bus'
- GFM = 'gfm'
- GFL = 'gfl'
- SYNCHRONOUS_MACHINE = 'sm'
- class g2elin_core.network.schema.GfmController(*values)[source]
-
Outer power-control law for a GFM unit, matching
GFM_P_controlin script_generic.m.- DROOP = 'droop'
- DROOP_FILTERED = 'droop_filtered'
- DVOC = 'dvoc'
- VSM = 'vsm'
- MATCHING = 'matching'
- class g2elin_core.network.schema.ExciterModel(*values)[source]
-
Which AVR/exciter a synchronous machine carries.
See
docs/sphinx/design/controller-models.mdfor the equations and for how these relate to the IEEE 421.5 library.- G2ELIN = 'g2elin'
terminal-voltage transducer, first-order amplifier, first-order exciter and a rate feedback.
- Type:
The regulator this tool has always had
- KUNDUR = 'kundur'
a thyristor exciter – transducer, gain and transient gain reduction, with no exciter lag. IEEE ST1A family.
- Type:
Kundur Fig. E12.9
- class g2elin_core.network.schema.PssModel(*values)[source]
-
Which power system stabiliser a synchronous machine carries, if any.
- G2ELIN = 'g2elin'
an input low-pass, a washout and two lead-lag stages.
- Type:
The stabiliser this tool has always had
- KUNDUR = 'kundur'
gain, washout and two lead-lag stages, taking the speed deviation unfiltered.
- Type:
Kundur Fig. E12.9
- NONE = 'none'
No stabiliser fitted.
- class g2elin_core.network.schema.GovernorModel(*values)[source]
-
Whether a synchronous machine’s prime mover responds to speed.
- G2ELIN = 'g2elin'
Droop into a first-order lag, the governor this tool has always had.
- NONE = 'none'
constant mechanical power, which is what most textbook small-signal examples assume.
- Type:
No governor
- class g2elin_core.network.schema.Bus(*, id, name='', vn_kv)[source]
Bases:
BaseModelA network node. Corresponds to one row of
Y_network.- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.Line(*, from_bus, to_bus, r_pu, x_pu, b_pu, length_km=1.0, name='', from_closed=True, to_closed=True)[source]
Bases:
BaseModelA pi-model transmission line, in per unit on the network’s
sn_mvabase.Corresponds to one row of
Y_line(excluding transformer rows, whichnetwork_form.mappends to the same matrix but are kept separate here asTransformer).- Parameters:
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.Transformer(*, hv_bus, lv_bus, r_pu, x_pu, sn_mva, tap_ratio=1.0, shift_degree=0.0, name='', hv_closed=True, lv_closed=True)[source]
Bases:
BaseModelA two-winding transformer with leakage impedance only (no magnetizing branch).
Matches
Y_TR’s simple R+jX model, built from a DER unit’sTR_R/TR_XLcolumns.Two quite different roles share this one element, told apart by whether a unit sits on the LV bus (
network/breakers.unit_transformers):a unit step-up, whose LV bus carries a DER. Its impedance is part of that unit’s own model – the classic machine-behind-transformer- impedance – and its LV bus never appears in the dynamic model.
a branch transformer between two grid buses, which is a branch of the network like a line, and gets a block of its own.
- Parameters:
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.Load(*, bus, p_mw, q_mvar, name='', closed=True)[source]
Bases:
BaseModelA constant-power load. Corresponds to the P_cons/Q_cons columns of
Y_network.- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.Shunt(*, bus, q_mvar, r_pu=None, closed=True, name='')[source]
Bases:
BaseModelA shunt compensation device at a bus: a capacitor bank or a reactor.
The sign convention is
Load’s (and pandapower’s):q_mvar > 0absorbs reactive power (a reactor),q_mvar < 0generates it (a capacitor bank).The two are modelled very differently, because a bus’s own dynamic model (
components/node.py) already is a capacitance to ground –C dv/dt = i - jwC v. A capacitor bank is simply more of that capacitance, so it adds no states at all. A reactor is an inductor to ground and cannot be folded in: a negative capacitance would flip the sign of the node’s dynamics, and netting the two against each other would only be right at exactly the nominal frequency. It therefore gets a branch of its own, the same RL branch a line uses with its far end at zero volts.- Parameters:
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.StateMode(*values)[source]
-
What becomes of one state group when the model order is lowered – see
g2elin_core.components.base.apply_reduction().- DYNAMIC = 'dynamic'
- ALGEBRAIC = 'algebraic'
- FROZEN = 'frozen'
- class g2elin_core.network.schema.ModelOptions(*, network_level='full', network_states=<factory>, network_frequency='frame', sm_level='full', sm_states=<factory>, gfm_level='full', gfm_states=<factory>, gfl_level='full', gfl_states=<factory>)[source]
Bases:
BaseModelWhich dynamics this network’s models keep.
Lives on the
Networkrather than on each analysis request on purpose: the level is part of what a saved case is, it travels with a network that is exported and re-imported, and the analysis layer’s model cache is keyed on the network’s own JSON, so changing a level correctly invalidates every cached model without any extra plumbing.*_levelnames a preset fromg2elin_core.reduction;*_statesoverrides individual state groups on top of it. A unit can override both again throughDerUnit.level/DerUnit.states.- Parameters:
- network_frequency: Literal['frame', 'nominal']
- property network_is_dynamic: bool
True when any passive element still integrates something – the property that decides whether a run is electromagnetic-transient.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.DerUnit(*, id, bus, unit_type, bus_type, v_set_pu, angle_set_deg=0.0, p_set_mw, q_set_mvar=0.0, p_cons_mw=0.0, q_cons_mvar=0.0, controller=None, exciter=None, pss=None, governor=None, xd_pu=None, sn_mva=None, closed=True, params=<factory>, level=None, states=<factory>)[source]
Bases:
BaseModelA dispatchable unit (infinite bus, GFM, GFL or synchronous machine).
Corresponds to one row of
Y_DER. Sits on its own bus (bus), connected to the rest of the network throughtransformer— matchingnetwork_form.m, which always places DER units behind a step-up transformer on a new node.- Parameters:
id (int)
bus (int)
unit_type (UnitType)
bus_type (BusType)
angle_set_deg (float)
p_set_mw (float)
q_set_mvar (float)
p_cons_mw (float)
q_cons_mvar (float)
controller (GfmController | None)
exciter (ExciterModel | None)
pss (PssModel | None)
governor (GovernorModel | None)
xd_pu (float | None)
closed (bool)
level (str | None)
- controller: GfmController | None
- exciter: ExciterModel | None
- governor: GovernorModel | None
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class g2elin_core.network.schema.Network(*, name, f_hz=60.0, sn_mva=100.0, units_use_first_transformer=False, nodes_share_first_line_b=False, min_node_b_pu=None, frame_follows_slack=False, models=<factory>, buses, lines=<factory>, transformers=<factory>, loads=<factory>, shunts=<factory>, der_units=<factory>)[source]
Bases:
BaseModelA complete network case: base values, topology, loads and DER dispatch.
- Parameters:
name (str)
f_hz (float)
sn_mva (float)
units_use_first_transformer (bool)
nodes_share_first_line_b (bool)
frame_follows_slack (bool)
models (ModelOptions)
transformers (list[Transformer])
- models: ModelOptions
- transformers: list[Transformer]
- unit_group_modes(der)[source]
{group id: mode}for one unit: the network’s default for its type, with the unit’s ownlevel/statesapplied on top.
- static unit_element(der)[source]
The reduction catalogue for one unit. A machine’s AVR and PSS groups depend on which regulator models it carries, so the catalogue is per unit rather than per type.
- Parameters:
der (DerUnit)
- Return type:
- frequency_is_regulated()[source]
Whether anything in this network holds the system frequency.
A machine’s governor does, a grid-forming converter’s droop law does, and an infinite bus pins it outright. A grid-following converter does not – it follows whatever frequency it is given – and the loads here are constant-power, so they contribute nothing either.
With none of them the common frequency is a free integrator: nudge every machine’s speed together and nothing brings it back. That leaves the model a marginal direction which is not an instability but the absence of a control loop, and it is why a stability verdict has to know (see
g2elin_core.modal.analysis.unregulated_frequency_mode()).- Return type:
- unit_level(der)[source]
The named level this unit’s settings correspond to, or None when its per-group overrides don’t match any.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- model_post_init(context, /)
This function is meant to behave like a BaseModel method to initialize private attributes.
It takes context as an argument since that’s what pydantic-core passes when calling it.
- Parameters:
self (BaseModel) – The BaseModel instance.
context (Any) – The context.
- Return type:
None
Preset network cases, ported from the MATLAB toolbox’s preset_networks.m.
Each preset is transcribed directly from the corresponding MATLAB source so it can be diffed against the original if the values there ever change.
The WSCC and CIGRE-islanded families each share one raw topology across
several DER-mix variants (WSCC/script_WSCC.m’s switch model.name and
CIGRE/script_CIGRE_Islanded.m’s switch model.name both only vary the
Y_DER table between cases, never the underlying buses/lines/loads) — so
each family is a shared _*_topology() builder plus one thin function per
variant supplying just its own DER rows, rather than duplicating the shared
part 8 (WSCC) or 4 (CIGRE) times over.
One deliberate departure from the MATLAB source. These presets no longer
set nodes_share_first_line_b: every bus gets its own capacitance – half
the charging of the lines actually meeting at it, plus its capacitor banks –
rather than all of them borrowing the first line’s. The MATLAB convention is
still available on any Network, and the tests that exist to prove this
port reproduces the toolbox set it explicitly; it is simply no longer what a
user gets by default, for two reasons:
It is a convention, not physics. A bus’s capacitance decides how fast its voltage settles, and with a shared value that speed is fictitious.
It is incompatible with a quasi-stationary network (
g2elin_core.reduction), where the bus equation stops being about speed and starts deciding where the voltage sits. A susceptance the power flow never saw then moves the answer – measurably: 0.03 pu on WSCC-9 (seenetwork/validation._model_order_issues).
- g2elin_core.network.presets.wscc9_3sm()[source]
The classic WSCC 9-bus system, 3 synchronous machines.
WSCC/script_WSCC.m, case'WSCC_3SM'. Bus numbering matches the MATLAB tool exactly: 7-9 are the LV generator terminal buses (behind their transformers), in DER declaration order.- Return type:
- g2elin_core.network.presets.wscc9_2sm_1gfl()[source]
WSCC 9-bus, 2 synchronous machines + 1 grid-following converter.
WSCC/script_WSCC.m, case'WSCC_2SM_1GFL'.- Return type:
- g2elin_core.network.presets.wscc9_1sm_2gfl()[source]
WSCC 9-bus, 1 synchronous machine + 2 grid-following converters.
WSCC/script_WSCC.m, case'WSCC_1SM_2GFL'.- Return type:
- g2elin_core.network.presets.wscc9_1sm_1gfm_1gfl()[source]
WSCC 9-bus, 1 synchronous machine + 1 grid-forming (Droop) + 1 grid-following converter.
WSCC/script_WSCC.m, case'WSCC_1SM_1GFM_1GFL'.- Return type:
- g2elin_core.network.presets.wscc9_1sm_2gfm()[source]
WSCC 9-bus, 1 synchronous machine + 2 grid-forming (Droop) converters.
WSCC/script_WSCC.m, case'WSCC_1SM_2GFM'.- Return type:
- g2elin_core.network.presets.wscc9_2sm_1gfm()[source]
WSCC 9-bus, 2 synchronous machines + 1 grid-forming (Droop) converter.
WSCC/script_WSCC.m, case'WSCC_2SM_1GFM'. Declared in the same order as the MATLAB source (slack SM at node 1, GFM at node 3, second SM at node 2) – note the second SM row there carries a realXd(0.2), unlike the other variants’ second SM row, transcribed as given rather than “corrected” to match.- Return type:
- g2elin_core.network.presets.wscc9_1gfm_2gfl()[source]
WSCC 9-bus, 1 grid-forming (Droop) converter + 2 grid-following converters.
WSCC/script_WSCC.m, case'WSCC_1GFM_2GFL'.Power flow only – the slack unit here is a GFM, and this project’s interconnection wiring (
interconnect/network_assembly.py) only supports a synchronous-machine or infinite-bus slack so far (a GFM slack behaves fundamentally differently – it doesn’t have a P/Q dispatch to hold, it defines the voltage/frequency reference the rest of the network locks to – and needs real modeling work, not just wiring). Power flow works fine (pandapower’s slack handling doesn’t care about unit type), but modal analysis / EMT raiseNotImplementedError(surfaced as an HTTP 501 by the API), the same documented, controlled failure mode as any other unsupported DER combination in this codebase.- Return type:
- g2elin_core.network.presets.wscc9_3gfm()[source]
WSCC 9-bus, 3 grid-forming (Droop) converters.
WSCC/script_WSCC.m, case'WSCC_3GFM'. Power flow only – same GFM-slack limitation aswscc9_1gfm_2gfl(), see its docstring.- Return type:
- g2elin_core.network.presets.cigre_islanded_1sm_1gfm_1gfl()[source]
CIGRE MV feeder, islanded, 1 SM + 1 GFM (Droop) + 1 GFL.
CIGRE/script_CIGRE_Islanded.m, case'CIGRE_Islanded_1SM_1GFM_1GFL'.- Return type:
- g2elin_core.network.presets.cigre_islanded_1sm_2gfm_1gfl()[source]
CIGRE LV/MV benchmark feeder, islanded, 1 SM + 2 GFM + 1 GFL.
Ported from
Functions/CIGRE_raw.mcombined with the'CIGRE_Islanded_1SM_2GFM_1GFL'case ofCIGRE/script_CIGRE_Islanded.m(the DER dispatch) and assembled the wayFunctions/network_form.mdoes. This is the case matching the Simulink model actually present in this repo,CIGRE/CIGRE_Islanded_1SM_2GFM_1GFL.slx.Bus numbering matches the MATLAB tool: 1-14 are the CIGRE feeder’s raw MV nodes, 15-18 are the LV DER terminal buses (behind their transformers), in DER declaration order (SM, GFM, GFM, GFL).
- Return type:
- g2elin_core.network.presets.cigre_islanded_1sm_3gfm_1gfl()[source]
CIGRE MV feeder, islanded, 1 SM + 3 GFM (Droop) + 1 GFL.
CIGRE/script_CIGRE_Islanded.m, case'CIGRE_Islanded_1SM_3GFM_1GFL'.- Return type:
- g2elin_core.network.presets.cigre_islanded_2sm_2gfm_2gfl()[source]
CIGRE MV feeder, islanded, 2 SM + 2 GFM (Droop) + 2 GFL.
CIGRE/script_CIGRE_Islanded.m, case'CIGRE_Islanded_2SM_2GFM_2GFL'– note the second SM row there also carries a realXd(0.2), unlike the single-SM variants’ non-slack SM rows elsewhere; transcribed as given.- Return type:
- g2elin_core.network.presets.sm_smib()[source]
Single-machine-infinite-bus: one synchronous machine against an infinite bus.
Not a straight MATLAB port, unlike every other preset in this module.
Functions/preset_networks.mnames this case ('SM_SMIB', alongside'GFM_SMIB'/'GFL_SMIB') but itsswitchbody is empty — the driving script was never finished in the original tool. The topology, line, and load values below are real, transcribed fromFunctions/SMIB_raw.m(confirmed to share CIGRE’s own base-value convention numerically, not just by inspection — see the module-level comment above). What’s genuinely reconstructed rather than ported: the DER-unit attachment (which bus, which transformer impedance, dispatch) following this project’s ownnetwork_form.mconvention (a DER behind its own transformer, on a new bus) and reusing CIGRE’s real DER-transformer impedance (0.0002/0.0240 pu) since no SMIB-specific value exists to transcribe.This is also the first preset to actually exercise
g2elin_core.components.ib(the infinite-bus component was built early in this project but had no real test case to wire into the interconnection until this).- Return type:
- g2elin_core.network.presets.gfm_smib()[source]
Single grid-forming converter (Droop control) against an infinite bus. See
sm_smib()for what’s ported vs. reconstructed here.- Return type:
- g2elin_core.network.presets.gfl_smib()[source]
Single grid-following converter against an infinite bus. See
sm_smib()for what’s ported vs. reconstructed here.- Return type:
- g2elin_core.network.presets.sm_smsm()[source]
One synchronous machine (PV, 1 MW) against a synchronous machine acting as the grid (slack).
- Return type:
- g2elin_core.network.presets.gfm_smsm()[source]
One grid-forming converter (Droop, PV, 1 MW) against a synchronous machine acting as the grid (slack).
- Return type:
- g2elin_core.network.presets.gfl_smsm()[source]
One grid-following converter (PQ, 1 MW, absorbing 0.9 MVAr) against a synchronous machine acting as the grid (slack).
Unlike
gfl_smib(), the GFL absorbs reactive power: the 61 km SMIB line’s charging (~1 MVAr) has to go somewhere, and a GFL at Q = 0 leaves all of it to the grid machine. An infinite bus absorbs that without complaint; a synchronous machine run that far under-excited is small-signal unstable (a real mode in its q-axis damper flux, +0.80 1/s, with the GFL terminal at 1.147 pu). At -0.9 MVAr the grid machine runs near unity power factor, voltages stay within 1.034 pu and the case is stable – sweep the GFL’s Q setpoint towards 0 on the root-locus page to watch that mode cross.- Return type:
- g2elin_core.network.presets.cigre_interconnected_1sm_1gfm_1gfl()[source]
CIGRE MV feeder, interconnected with the upstream grid, 1 SM + 1 GFM (Droop) + 1 GFL.
Functions/preset_networks.mnames this case ('CIGRE_Interconnected_1SM_1GFM_1GFL') but, like the SMIB cases, its driving script was never written. Ported: the interconnected topology and loads (CIGRE_raw.m,S0=1) and the base values. Reconstructed: the upstream grid, modelled as an infinite bus (the slack) behind a transformer at feeder node 1 – given the same impedance as the unit transformers, so all four transformers are identical – and the units placed and dispatched as incigre_islanded_1sm_1gfm_1gfl()– with the SM now a PV unit, since the grid takes the slack role.The grid terminal bus is modelled at MV (20 kV, the grid referred to the feeder side), so the substation transformer keeps the “unit on its own LV-side terminal bus” convention every unit here follows.
- Return type:
- g2elin_core.network.presets.kundur_machine_params(h_s)[source]
Kundur’s two-area machine, in this model’s flux-linkage parameters.
The book gives the machine the usual way – Xd/Xq/Xl, the transient and subtransient reactances and their open-circuit time constants – while
components/sm.pyis written in mutual and leakage inductances with explicit field and damper windings. The standard relations convert one to the other:Lad = Xd - XlandLaq = Xq - Xl(mutual = synchronous minus leakage)X' = Xl + Lad||LfdandX'' = Xl + Lad||Lfd||L1d(a winding in parallel shows up as a smaller effective reactance the faster you look)T'do = (Lad + Lfd) / (wb Rfd), and likewise for each damper
The result is per unit of the machine’s 900 MVA rating, which is why the units carry
sn_mva– the model rebases it onto the network’s 100 MVA (seeoperating_point.rebase_params).tests/test_kundur.pyinverts these relations and checks the published numbers come back.
- g2elin_core.network.presets.kundur_two_area()[source]
Kundur’s two-area system with Example 12.9’s case (iv) regulators: a thyristor exciter with high transient gain plus a power-system stabilizer, which damps the inter-area mode.
Case (iii) – transient gain reduction instead of a stabilizer – is the same network with
TA = 1.0and the stabilizer set tonone. On this network that is not a fix: see the note above on which example supplies the network and which supplies the regulator data.- Return type:
- g2elin_core.network.presets.kundur_two_area_classic()[source]
Kundur’s two-area system under the book’s own assumptions: a thyristor exciter with high transient gain, no stabilizer and constant mechanical torque.
This is the case the example exists for – the inter-area mode comes out negatively damped, so the system is unstable until a stabilizer is added.
- Return type:
- g2elin_core.network.presets.ieee14()[source]
IEEE 14-bus test case: 5 machines (two of them synchronous condensers), three tapped transformers and a capacitor bank.
- Return type:
- g2elin_core.network.presets.ieee39()[source]
IEEE 39-bus “New England” test case: 10 machines, 34 lines and 11 tapped transformers – the standard mid-size case for transient stability.
- Return type:
- g2elin_core.network.presets.ieee118()[source]
IEEE 118-bus test case: 54 machines and 14 shunts. Large enough that modal analysis is slow and EMT is impractical; power flow is quick.
- Return type:
Computes a 2D layout for a Network’s
buses/lines/transformers, for the web UI’s Network and Power Flow tabs.
A DER unit or load in this schema’s own convention isn’t a separate
geometric node — a DER sits on its own real Bus (network_form.m
always gives it one, behind its own transformer), and a load just
references an existing bus id. So the graph to lay out is exactly
network.buses (nodes) + network.lines/network.transformers
(edges); DER/load presence is metadata decorating an existing bus node,
not a node of its own.
- class g2elin_core.network.topology.BusNode(id: 'int', name: 'str', vn_kv: 'float', x: 'float', y: 'float', unit_type: 'str | None', load_p_mw: 'float | None', der_info: 'dict | None' = None)[source]
Bases:
object- Parameters:
- class g2elin_core.network.topology.TopologyEdge(kind: 'str', from_bus: 'int', to_bus: 'int', name: 'str', params: 'dict' = <factory>)[source]
Bases:
object
- class g2elin_core.network.topology.NetworkTopology(nodes: 'list[BusNode]', edges: 'list[TopologyEdge]')[source]
Bases:
object- Parameters:
edges (list[TopologyEdge])
- edges: list[TopologyEdge]
- g2elin_core.network.topology.compute_topology_layout(network)[source]
Kamada-Kawai layout (a graph-distance-preserving spring layout, reads well for the mostly-tree-like/lightly-meshed topologies these presets have) over the bus graph, normalized to roughly [-1, 1] per axis. Falls back to a spring layout if Kamada-Kawai can’t be computed (e.g. a disconnected graph, which none of the current presets produce, but nothing here assumes it can’t happen for a future one).
- Parameters:
network (Network)
- Return type:
Structural pre-flight checks for a Network
– catches problems that would otherwise surface one at a time as a crash
(or a confusing wrong answer) deep inside power flow, modal analysis, or
EMT simulation, and reports every issue found in one pass instead of
just the first.
Every hand-crafted preset in this codebase satisfies all of these by
construction; a network assembled by hand (the web UI’s network editor /
drag-and-drop builder) can easily violate any of them. pydantic’s own
validators on Network already guarantee two structural properties
unconditionally (every bus reference resolves, exactly one slack DER) –
this module covers everything else: things that are syntactically valid
Network JSON but still make power flow, modal analysis, or EMT
simulation fail, or silently misbehave, downstream.
- class g2elin_core.network.validation.NetworkIssue(severity: 'str', message: 'str', affects: 'tuple[str, ...]')[source]
Bases:
object
- g2elin_core.network.validation.validate_network(network)[source]
Every structural problem found, not just the first. An empty list means the network is safe to run power flow, modal analysis, and EMT simulation on – modulo the actual numerics still converging for a given operating point, which no static check can guarantee.
- Parameters:
network (Network)
- Return type: