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 Rt/Lt – the classic machine behind a transformer impedance

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

tap_ratio / shift_degree

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

\[v' = \frac{v_{hv}}{a\,e^{j\varphi}},\qquad i_{hv} = \frac{i\,e^{j\varphi}}{a}\]

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’s switch model.name, one case per 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 raise NotImplementedError (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 own switch 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)

kundur_two_area (this tool’s AVR + PSS + governor)

0.71 Hz, +12.9%

1.23 / 1.26 Hz

kundur_two_area_classic (the book’s: fast exciter, no PSS, constant torque)

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]

Bases: str, Enum

Load-flow bus type, matching Y_network column 4 / Y_DER column 4.

SLACK = 'slack'
PV = 'pv'
PQ = 'pq'
class g2elin_core.network.schema.UnitType(*values)[source]

Bases: str, Enum

DER unit type, matching Y_DER column 2 (Y_network column 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]

Bases: str, Enum

Outer power-control law for a GFM unit, matching GFM_P_control in script_generic.m.

DROOP = 'droop'
DROOP_FILTERED = 'droop_filtered'
DVOC = 'dvoc'
VSM = 'vsm'
MATCHING = 'matching'
class g2elin_core.network.schema.ExciterModel(*values)[source]

Bases: str, Enum

Which AVR/exciter a synchronous machine carries.

See docs/sphinx/design/controller-models.md for 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]

Bases: str, Enum

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]

Bases: str, Enum

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: BaseModel

A network node. Corresponds to one row of Y_network.

Parameters:
id: int
name: str
vn_kv: float
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: BaseModel

A pi-model transmission line, in per unit on the network’s sn_mva base.

Corresponds to one row of Y_line (excluding transformer rows, which network_form.m appends to the same matrix but are kept separate here as Transformer).

Parameters:
from_bus: int
to_bus: int
r_pu: float
x_pu: float
b_pu: float
length_km: float
name: str
from_closed: bool
to_closed: bool
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: BaseModel

A two-winding transformer with leakage impedance only (no magnetizing branch).

Matches Y_TR’s simple R+jX model, built from a DER unit’s TR_R / TR_XL columns.

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:
hv_bus: int
lv_bus: int
r_pu: float
x_pu: float
sn_mva: float
tap_ratio: float
shift_degree: float
name: str
hv_closed: bool
lv_closed: bool
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: BaseModel

A constant-power load. Corresponds to the P_cons/Q_cons columns of Y_network.

Parameters:
bus: int
p_mw: float
q_mvar: float
name: str
closed: bool
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: BaseModel

A shunt compensation device at a bus: a capacitor bank or a reactor.

The sign convention is Load’s (and pandapower’s): q_mvar > 0 absorbs reactive power (a reactor), q_mvar < 0 generates 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:
bus: int
q_mvar: float
r_pu: float | None
closed: bool
name: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class g2elin_core.network.schema.StateMode(*values)[source]

Bases: str, Enum

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: BaseModel

Which dynamics this network’s models keep.

Lives on the Network rather 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.

*_level names a preset from g2elin_core.reduction; *_states overrides individual state groups on top of it. A unit can override both again through DerUnit.level / DerUnit.states.

Parameters:
network_level: str
network_states: dict[str, StateMode]
network_frequency: Literal['frame', 'nominal']
sm_level: str
sm_states: dict[str, StateMode]
gfm_level: str
gfm_states: dict[str, StateMode]
gfl_level: str
gfl_states: dict[str, StateMode]
modes_for(kind)[source]

{symbol name: mode} for one element type.

Parameters:

kind (str)

Return type:

dict[str, str]

group_modes_for(kind)[source]

{group id: mode} for one element type – what the UI shows.

Parameters:

kind (str)

Return type:

dict[str, str]

property network_is_dynamic: bool

True when any passive element still integrates something – the property that decides whether a run is electromagnetic-transient.

property fixed_network_frequency: bool
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: BaseModel

A 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 through transformer — matching network_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
v_set_pu: float
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
sn_mva: float | None
closed: bool
params: dict[str, float]
level: str | None
states: dict[str, StateMode]
property controller_model: str

This converter’s outer power-control law, defaulted.

property exciter_model: str

This machine’s exciter, defaulted – what the model builders take.

property pss_model: str

This machine’s stabiliser, defaulted.

property governor_model: str

This machine’s governor, defaulted.

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: BaseModel

A 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
min_node_b_pu: float | None
frame_follows_slack: bool
models: ModelOptions
buses: list[Bus]
lines: list[Line]
transformers: list[Transformer]
loads: list[Load]
shunts: list[Shunt]
der_units: list[DerUnit]
bus(bus_id)[source]
Parameters:

bus_id (int)

Return type:

Bus

unit_group_modes(der)[source]

{group id: mode} for one unit: the network’s default for its type, with the unit’s own level/states applied on top.

Parameters:

der (DerUnit)

Return type:

dict[str, str]

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:

ElementModel

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:

bool

unit_modes(der)[source]

{symbol name: mode} for one unit – what its *_dae() builder takes.

Parameters:

der (DerUnit)

Return type:

dict[str, str]

unit_level(der)[source]

The named level this unit’s settings correspond to, or None when its per-group overrides don’t match any.

Parameters:

der (DerUnit)

Return type:

str | None

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 (see network/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:

Network

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:

Network

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:

Network

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:

Network

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:

Network

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 real Xd (0.2), unlike the other variants’ second SM row, transcribed as given rather than “corrected” to match.

Return type:

Network

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 raise NotImplementedError (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:

Network

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 as wscc9_1gfm_2gfl(), see its docstring.

Return type:

Network

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:

Network

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.m combined with the 'CIGRE_Islanded_1SM_2GFM_1GFL' case of CIGRE/script_CIGRE_Islanded.m (the DER dispatch) and assembled the way Functions/network_form.m does. 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:

Network

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:

Network

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 real Xd (0.2), unlike the single-SM variants’ non-slack SM rows elsewhere; transcribed as given.

Return type:

Network

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.m names this case ('SM_SMIB', alongside 'GFM_SMIB'/'GFL_SMIB') but its switch body is empty — the driving script was never finished in the original tool. The topology, line, and load values below are real, transcribed from Functions/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 own network_form.m convention (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:

Network

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:

Network

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:

Network

g2elin_core.network.presets.sm_smsm()[source]

One synchronous machine (PV, 1 MW) against a synchronous machine acting as the grid (slack).

Return type:

Network

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:

Network

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:

Network

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.m names 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 in cigre_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:

Network

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.py is written in mutual and leakage inductances with explicit field and damper windings. The standard relations convert one to the other:

  • Lad = Xd - Xl and Laq = Xq - Xl (mutual = synchronous minus leakage)

  • X' = Xl + Lad||Lfd and X'' = 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 (see operating_point.rebase_params). tests/test_kundur.py inverts these relations and checks the published numbers come back.

Parameters:

h_s (float)

Return type:

dict[str, float]

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.0 and the stabilizer set to none. 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:

Network

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:

Network

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:

Network

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:

Network

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:

Network

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:
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
class g2elin_core.network.topology.TopologyEdge(kind: 'str', from_bus: 'int', to_bus: 'int', name: 'str', params: 'dict' = <factory>)[source]

Bases: object

Parameters:
kind: str
from_bus: int
to_bus: int
name: str
params: dict
class g2elin_core.network.topology.NetworkTopology(nodes: 'list[BusNode]', edges: 'list[TopologyEdge]')[source]

Bases: object

Parameters:
nodes: list[BusNode]
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:

NetworkTopology

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

Parameters:
severity: str
message: str
affects: tuple[str, ...]
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:

list[NetworkIssue]