components — symbolic nonlinear-then-linearized models
Ports the pattern used throughout Functions/sym*.m: build a full
nonlinear DAE symbolically with sympy
(differential equations, algebraic constraints, outputs), then either
linearize it around an operating point (what every MATLAB sym*.m file
did, and all it did) or keep it nonlinear and lambdify it to a fast numeric
callable (new — this is what makes EMT simulation
possible without re-deriving any physics).
Per-component modules
Each ported component follows the same shape: a module-level *_dae()
function (cached with @lru_cache, since building the symbolic DAE is the
expensive part and every instance of a given component type shares the same
equations), an *OperatingPoint class that turns power-flow results into
substitution values, and linearize_*() / *_nonlinear_point() /
*_nonlinear_funcs() / *_nonlinear_jacobians() wrapper functions.
flowchart LR
subgraph Sources["Functions/sym*.m"]
symSG["symSG.m + SG_subs.m"]
symGFM["symGFM_types.m + GFM_subs.m"]
symGFL["symGFL.m + GFL_subs.m"]
symIB["symIB.m + IB_subs.m"]
symLine["symLine.m"]
symNode["symNode.m"]
symLoad["symLoad.m"]
end
subgraph Python["components/*.py"]
sm["sm.py<br/>synchronous machine"]
gfm["gfm.py<br/>grid-forming"]
gfl["gfl.py<br/>grid-following"]
ib["ib.py<br/>infinite bus"]
frame["frame.py<br/>reference frame"]
line["line.py"]
node["node.py"]
load["load.py"]
end
symSG --> sm --> DAE["build_dae() -> ComponentDAE"]
symGFM --> gfm --> DAE
symGFL --> gfl --> DAE
symIB --> ib --> DAE
symLine --> line --> DAE
symNode --> node --> DAE
symLoad --> load --> DAE
Coverage note: gfm.py implements only the 'Droop' branch of
symGFM_types.m (the CIGRE preset’s GFM controller); 'Droop+filter',
'Droop+PLL', 'dVOC', 'VSM' and 'Matching' aren’t ported.
ib.py (infinite bus) was built and tested in isolation for a while
before being wired into interconnect — no MATLAB
script in the source repo defines a complete infinite-bus DER case
(preset_networks.m names SM_SMIB/GFM_SMIB/GFL_SMIB but their
switch bodies are empty), so there was no real preset to validate it
against until the SMIB presets (network) reconstructed
one from the real topology data that does exist (SMIB_raw.m).
A major bug was found and fixed while building sm.py’s
SmOperatingPoint: a double-rotation error in the terminal-voltage
phasor (v0 * exp(j*(angle_terminal_rad - theta0)) instead of
v0 * exp(-j*theta0)) silently corrupted every non-slack synchronous
machine’s operating point, invisible on the slack unit only because
pandapower pins its angle to exactly 0. See the SmOperatingPoint
docstring below and its internal consistency assertion (comparing
power-flow-derived id0/iq0 against independently-derived values from
the flux-current relations) — that assertion is what caught it.
Physics: the full nonlinear equations
Everything below is the literal content of each *_dae() function — the
same diffeqVec/algeqVec/outputeqVec sympy builds, transcribed to
LaTeX rather than paraphrased, so this can be read as the model
specification and cross-checked line-by-line against the source. Two
notational conventions hold throughout, matching pu_base’s amplitude-invariant convention and the MATLAB
source’s own dq-frame formulation:
Everything is per-unit on the network’s own
sn_mva/busvn_kvbase (pu_base), and every state/algebraic/input symbol below is exactly the sympy symbol name used in the code, just set in math instead ofsnake_case(e.g.Dwr→ \(\Delta\omega_r\),phi_d→ \(\psi_d\),theta_g→ \(\theta_g\)).Two reference frames are in play. A global dq frame carries every node, line and load (their states already have a
_gsuffix), turning at \(\omega_g\) with angle \(\theta_g\). Each DER (SM/GFM/GFL/IB) instead has its own rotating frame, at its own angle \(\theta\) (SM, GFM), \(\theta_{pll}\) (GFL, PLL-locked) or \(\theta_{up}\) (IB) — the algebraic equations below rotate a DER’s own terminal quantities into and out of the global frame by the angle difference \(\theta_g - \theta\).The global frame is a block of its own (frame.py): one state, the frame angle, following the speed of the unit its island is referenced to, and nothing else. It replaces the toolbox’s convention of reading \(\theta_g\)/\(\omega_g\) off the slack unit, which made that unit impossible to disconnect: with the frame standing apart, any unit can be tripped and a network can split into islands, each with its own frame and its own frequency.
Network.frame_follows_slackrestores the old wiring, where the slack unit itself is the frame and has to be a synchronous machine or an infinite bus. The two give the same modes (checked to 1e-5 relative intests/test_breakers.py) and, being only a change of coordinates, the same nonlinear trajectories.
A state’s time derivative is written \(\dot x\) throughout; every equation
below carries an explicit \(\omega_b\) (wb, the base angular frequency,
\(2\pi f_n\)) factor exactly where the code has it — per-unit flux-linkage
and current states pick up this factor because \(1\) per-unit flux
corresponds to \(1/\omega_b\) seconds of a unit voltage integrated, the
standard per-unit machine-equation convention.
Synchronous machine (sm.py)
A round-rotor synchronous machine with one field winding and 2 damper
windings (1 \(d\)-axis, 2 \(q\)-axis), its own step-up transformer, and a set of
regulators — 19 states in its default configuration, ported from
Functions/symSG.m.
States \(x = [i_{gd}, i_{gq}, \psi_d, \psi_q, \psi_{fd}, \psi_{1d}, \psi_{1q}, \psi_{2q}, \Delta\omega_r, \theta, \ldots]\) — grid-side transformer current, stator/field/damper flux linkages, speed deviation and rotor angle, followed by whatever the fitted regulators carry (below).
Selectable regulators
The exciter, the stabiliser and the governor are chosen per unit through
DerUnit.exciter, DerUnit.pss and DerUnit.governor. They decide the
machine’s states and its parameter set: each model brings its own
parameters under its own names, so what is typed is what that model’s own
diagram shows, and an override under a name the fitted model does not have
is rejected rather than ignored.
Slot |
Model |
States |
Parameters |
|---|---|---|---|
Exciter |
|
\(e_1, e_2, e_{fd}, e_3\) |
\(T_r, K_a, T_a, K_e, T_e, K_{fd}, T_{fd}\) |
|
\(e_1, e_{tgr}\) |
\(T_R, K_A, T_A, T_B\) |
|
PSS |
|
\(\Delta\omega_1, v_1, v_2, v_{pss}\) |
\(K_{PSS}, T_{LP}, T_{HP}, T_{1n}, T_{1d}, T_{2n}, T_{2d}\) |
|
\(v_1, v_2, v_{pss}\) |
\(K_{STAB}, T_W, T_1, T_2, T_3, T_4\) |
|
|
— |
— |
|
Governor |
|
\(P_m\) |
\(m_p, T_G\) |
|
— |
— |
The kundur pair is Kundur Fig. E12.9: a thyristor exciter — a terminal
voltage transducer, a gain and transient gain reduction, with no exciter lag
— and a stabiliser that is a washout and two lead-lag stages. Because a
thyristor bridge has no time constant of its own, \(E_{fd}\) follows the
regulator instantaneously and is algebraic there rather than a state,
which is why that exciter is two states shorter rather than one.
none is equipment that is not fitted, not a gain turned down: the states,
the parameters and the reduction group all go. A machine with no governor
holds its mechanical power at the operating point, which is the
constant-torque assumption most textbook small-signal examples are worked
under — and note that a fleet with no governor anywhere has an unregulated
common frequency, which the modal tools treat as marginal by construction
rather than as an instability.
Algebraic variables \(z = [\omega_r, v_{ed}, v_{eq}, i_d, i_{1d}, i_{fd}, i_q, i_{1q}, i_{2q}, v_{gd}, v_{gq}, C_m, C_e, \Delta P, E_t]\).
Inputs: own \(u_s = [V_{ref}, P_{ref}, \omega_{ref}]\) — note that \(P_{ref}\) is the governor’s setpoint, so with no governor fitted its column of \(B\) is exactly zero and it moves nothing; grid-coupling \(u_g = [\theta_g, v_{gd,g}, v_{gq,g}]\) (just \([v_{gd,g}, v_{gq,g}]\) for the slack unit, which needs no \(\theta_g\) input since \(\theta_g \equiv \theta\) for it by definition).
Electrical dynamics (transformer current, stator/damper flux):
Mechanical (swing equation + first-order turbine/governor):
PSS (washout \(\to\) lead-lag cascade — note \(\dot v_1\) feeds directly into the \(\dot v_2\) equation and \(\dot v_2\) into \(\dot v_{pss}\), a transfer-function realization, not three independent first-order lags):
AVR (voltage transducer \(\to\) PI-ish comparator \(\to\) exciter \(\to\) field-current-derivative feedback):
Algebraic constraints — frequency reference, stator-side aux-load Ohm’s law, flux/current inversion, torque/power, and the terminal-voltage magnitude:
Frame rotation into the machine’s own dq frame (non-slack only — the slack sets \(v_{gd} = v_{gd,g}\), \(v_{gq} = v_{gq,g}\) directly, since its own frame is the global one):
Outputs: own \([P_e, Q_e, \omega_r, \theta, \Delta\omega_r, E_t]\) with \(P_e = v_{ed}i_{gd}+v_{eq}i_{gq}\), \(Q_e=-v_{ed}i_{gq}+v_{eq}i_{gd}\); grid current, rotated back to the global frame for a non-slack unit (the slack outputs \(i_{gd}, i_{gq}\) directly, being already in that frame):
Default electrical/mechanical/AVR/PSS parameter values are tabulated in operating_point.
Grid-forming converter (gfm.py)
A two-level VSC behind an LCL filter (\(R_f/L_f\) series, \(C_f\) shunt) and its
own step-up transformer (\(R_t/L_t\)), with an outer power-control law, a
voltage-control loop, a current-control loop, and a first-order DC-link
model — 15 states running droop, ported from Functions/symGFM_types.m.
States \(x = [i_{sd}, i_{sq}, i_{gd}, i_{gq}, v_{dc}, i_{dc}, \ldots, M_{VLd}, M_{VLq}, M_{CLd}, M_{CLq}]\) — converter and grid-side filter currents, filter capacitor voltage, DC-link voltage and current, then the outer law’s own states (below), then the voltage-loop/current-loop PI integrator states.
Selectable power-control laws
All five of symGFM_types.m’s laws are chosen per unit through
DerUnit.controller. They differ in exactly three things — which states the
outer loop carries, how it forms the frequency deviation \(\Delta\omega\) that
the angle integrates, and how it forms the voltage reference the cascade
then tracks. Everything downstream is identical for all five.
Law |
Outer states |
\(\Delta\omega\) |
\(v_{ed}^{ref}\) |
Parameters |
|---|---|---|---|---|
|
\(p_m, \theta, q_m\) |
\(m_p(P_{ref}-p_m)\) |
\(V_{ref} + (Q_{ref}-q_m)n_q\) |
\(m_p, n_q, w_f\) |
|
\(p_m, \Delta\omega, \theta, q_m\) |
a state |
same |
\(m_p, n_q, w_f, w_c\) |
|
\(p_m, \theta, q_m, v_{ed}^{ref}\) |
\(\eta(P_{ref}/V_{ref}^2 - p_m/(v_{ed}^{ref})^2)\) |
a state |
\(\eta, \alpha, w_f\) |
|
\(\Delta\omega, \theta, \Phi\) |
a state (swing, \(J\)/\(D_p\)) |
\(\omega\Phi\) |
\(J, D_p, K, D_q\) |
|
\(v_{dc,m}, \theta\) |
\(K_\theta(v_{dc,m} - V_{dc,ref})\) |
\(V_{ref}\) |
\(K_\theta, w_f\) |
A virtual synchronous machine and matching control carry no power filters at all. VSM measures \(p\) and \(q\) directly — its emulated inertia is what smooths the response — and matching takes its frequency from the DC link, which is what a machine’s speed does physically. So \(p_m\)/\(q_m\) are absent from those two rather than merely retuned.
The gains are script_generic.m’s, and every one is written in terms of the
droop tuning (\(\eta = m_p\), \(J = 1/(m_p w_f)\), \(K_\theta = m_p K_{pdc}\), and
so on). That is deliberate: the laws are meant to be comparable, tuned to
the same equivalent inertia and reactive gain, so a study that swaps one for
another sees what the law changes rather than what a different tuning
changes.
Algebraic \(z = [m_d, m_q, \omega]\) (modulation index, own frequency). Inputs: \(u_s = [P_{ref}, Q_{ref}, V_{ref}, \omega_{ref}, V_{dc,ref}]\), \(u_g = [\theta_g, v_{gd,g}, v_{gq,g}]\) (a GFM is never the slack).
LCL filter + DC link:
Droop + voltage/current loops — \(P\)/\(\omega\) and \(Q\)/\(V\) droop, a low-pass power-measurement filter (\(\omega_f\)), then a voltage loop (PI + feed-forward, gains \(K_{p/i,VL}\)) whose output is the current reference, then a current loop (PI + feed-forward, gains \(K_{p/i,CL}\)):
Algebraic (modulation index, own frequency):
Outputs: \(p\), \(q\), \(\omega\), \(V_t=\sqrt{v_{ed}^2+v_{eq}^2}\), and the grid current rotated back to the global frame the same way as the SM’s non-slack case above (using \(\theta\) in place of the SM’s own angle).
Default filter/loop-tuning values (pole-placement formulas for \(K_{p/i,VL}\), \(K_{p/i,CL}\), \(K_{pdc}\)) are in operating_point.
Grid-following converter (gfl.py)
Structurally the same LCL-filter VSC as the GFM above, but current-
(not voltage-) controlled, phase-locked to the grid via an SRF-PLL
instead of self-clocking — 14 states, ported from Functions/symGFL.m.
Never the slack.
States \(x = [i_{sd}, i_{sq}, i_{gd}, i_{gq}, v_{ed}, v_{eq}, v_{dc}, i_{dc}, M_d, M_q, M_{CLd}, M_{CLq}, M_{pll}, \theta_{pll}]\). Algebraic \(z=[m_d, m_q, \omega_{pll}]\). Inputs: \(u_s=[V_{dc,ref}, Q_{ref}, I_{dc,ref}]\), \(u_g=[\theta_g, v_{gd,g}, v_{gq,g}]\).
LCL filter + DC link (identical form to the GFM’s, \(\omega\) replaced by the PLL’s own tracked frequency \(\omega_{pll}\)):
DC-voltage / reactive-power / current loops — a \(d\)-axis current reference regulates the DC-link voltage directly (no separate power loop); the \(q\)-axis current reference is the reactive-power loop’s own integrator state, fed straight into the current loop:
SRF-PLL — the integral-of-\(v_{eq}\) term drives \(v_{eq}\to 0\), locking \(\theta_{pll}\) to the terminal-voltage vector angle:
Algebraic:
Outputs: \(p\), \(q\), \(\omega_{pll}\) (recomputed at output as \(M_{pll}+K_{p,pll}v_{eq}+\omega_{ff}\) — algebraically identical to the constraint above, kept as a separate output expression in the source), \(V_t\), and grid current rotated back to the global frame using \(\theta_{pll}\).
Infinite bus (ib.py)
The simplest DER: an ideal voltage source of fixed magnitude \(V_{up}\) and
frequency \(\omega_{up}\) behind its own series impedance \(R_{up}/L_{up}\)
(the DER’s own transformer) — 3 states, ported from Functions/symIB.m.
No control loops, no algebraic variables at all. Always the slack (never
wired as a non-slack unit — see interconnect), so
its own frame is the global frame and its equations never rotate
anything.
States \(x=[i_{gd,g}, i_{gq,g}, \theta_{up}]\). Inputs: \(u_s=[\omega_{up}, V_{up}]\), \(u_g=[v_{gd,g}, v_{gq,g}]\).
Outputs: own \([P_{up}, Q_{up}] = [V_{up}i_{gd,g},\; -V_{up}i_{gq,g}]\); grid-facing \([i_{gd,g}, i_{gq,g}, \theta_{up}, \omega_{up}]\) (echoed directly — an IB defines the global frame, so nothing needs rotating).
Transmission line (line.py)
A series \(R_l\)–\(L_l\) branch between two buses \(j\) (from_bus) and \(k\)
(to_bus), living entirely in the global dq frame (no rotation, unlike
every DER above) — 2 states, ported from Functions/symLine.m.
States \(x=[i_{ld,g}, i_{lq,g}]\). Inputs: \([\omega_g, v_{gdj,g}, v_{gqj,g}, v_{gdk,g}, v_{gqk,g}]\).
Output: \([i_{ld,g}, i_{lq,g}]\) itself (this current is what interconnect’s node current-balance wiring sums into the two endpoint buses, \(+\) at \(k\), \(-\) at \(j\)). \(L_l\) plays the role of the line’s reactance \(x_{pu}\) directly, per-unit convention throughout this codebase.
Network node (node.py)
The shunt line-charging capacitance \(C_l\) seen at one bus — its “voltage”
state is that bus’s own dq voltage, so this component is what actually
defines bus voltage dynamically in the closed-loop system (every other
component treats its own bus voltage as an algebraic/input quantity) — 2
states, ported from Functions/symNode.m.
States \(x=[v_{gd,g}, v_{gq,g}]\). Input: \([\omega_g, i_{shd,g}, i_{shq,g}]\) — the net shunt current injected at this bus (KCL sum of every incident DER/line/load current, built by interconnect’s wiring rules).
Output: \([v_{gd,g}, v_{gq,g}]\) itself — every other component’s
vgd_g/vgq_g input is wired to this output.
Constant-impedance load (load.py)
A series \(R_c\)–\(L_c\) branch to (implicitly) a zero-voltage reference,
i.e. a constant-impedance load equivalent at its bus — 2 states, ported
from Functions/symLoad.m. \(R_c\)/\(L_c\) (load_rx in
operating_point) are derived from the load’s
\(P\)/\(Q\)/\(V\) at the power-flow operating point, not free parameters.
States \(x=[i_{cd,g}, i_{cq,g}]\). Input: \([\omega_g, v_{gd,g}, v_{gq,g}]\).
Output: \([i_{cd,g}, i_{cq,g}]\) — subtracted (load convention) at its bus’s current balance, the same way a line’s current is added/subtracted at its two endpoints.
Linearization, in one place
Every component above shares one linearization step
(linearize()), algebraic-
variable elimination by direct substitution into the Jacobians formed
once by build_dae():
evaluated numerically at each component’s own operating point (all the
\(0\)-subscripted values each *OperatingPoint class computes). This is
exactly the small-signal linearization every sym*.m file in the MATLAB
source performs — see the module’s own docstring above for the one
deliberate implementation difference (symbolic-vs-numeric order of
substitution and inversion, same result either way).
Reference
Shared machinery for linearized small-signal component models.
Ports the pattern used throughout Functions/sym*.m: build a full
nonlinear DAE symbolically (differential equations diffeqVec, algebraic
constraints algeqVec, outputs outputeqVec), take Jacobians to get
Fx, Fz, Fu, Gx, Gz, Gu, Hx, Hz, Hu, then eliminate the algebraic
variables:
A = Fx - Fz @ inv(Gz) @ Gx B = Fu - Fz @ inv(Gz) @ Gu C = Hx - Hz @ inv(Gz) @ Gx D = Hu - Hz @ inv(Gz) @ Gu
One deliberate deviation from the MATLAB source: the MATLAB toolbox
substitutes numeric parameter/operating-point values before forming
Ai = Fx - Fz*inv(Gz)*Gx (symbolic matrix inversion, feasible in MATLAB
because it’s cached per case). Substitution and matrix algebra commute, so
this module instead keeps Fx..Hu symbolic (cheap — just
differentiation, no inversion) and defers substitution to
ComponentDAE.linearize(), where the Gz inverse and matrix products
run numerically in numpy. Same result, avoids symbolic inversion of a
15x15+ matrix.
- class g2elin_core.components.base.LinearComponent(A, B, C, D, n_us, n_ug, n_out_s, n_out_g, state_names, input_names, output_names)[source]
Bases:
objectA numeric linearized state-space, split into “own” (s) and “grid” (g) inputs/outputs the way the MATLAB toolbox splits
nIP_*/nOP_*.usinputs are the component’s own setpoints/references (e.g. a generator’sP_ref);uginputs/outputs are the interconnection signals (node voltages, injected currents, the global reference angle) thatg2elin_core.interconnectwires between components.- Parameters:
- class g2elin_core.components.base.NonlinearFuncs(f, g, h)[source]
Bases:
objectNumeric (numpy-backed) callables for a component’s full nonlinear DAE, lambdified from the same symbolic equations the linearization was built from. All three take
(x, z, u, p)— numpy arrays matchingComponentDAE.state_syms/alg_syms/input_syms/param_symsorder — and return a 1-D numpy array.This is what makes EMT (nonlinear) time-domain simulation possible without re-deriving physics: it’s the same
diffeqVec/algeqVec/outputeqVeceverysym*.mfile builds before linearizing, kept instead of discarded (see the migration plan’s “the nonlinear models already exist” finding).
- class g2elin_core.components.base.NonlinearJacobians(Gz, Gx, Gu, Hz, Hx, Hu, Fx, Fz, Fu)[source]
Bases:
objectNumeric Jacobian callables for
f,gandh— seeComponentDAE.nonlinear_jacobians(). Each takes(x, z, u, p)and returns a 2-D numpy array.Fx/Fz/Fuare what an implicit ODE solver needs: with them the total derivativedf/dxalong the constraint manifold can be assembled directly (seeode_jacobian()) instead of being finite-differenced, which costs one full coupled Newton solve per state.- Parameters:
- class g2elin_core.components.base.ComponentDAE(Fx, Fz, Fu, Gx, Gz, Gu, Hx, Hz, Hu, n_us, n_ug, n_out_s, n_out_g, state_names=<factory>, input_names=<factory>, output_names=<factory>, state_syms=<factory>, alg_syms=<factory>, input_syms=<factory>, diffeq_exprs=<factory>, algeq_exprs=<factory>, output_exprs=<factory>, _cache=<factory>)[source]
Bases:
objectSymbolic Jacobians for one component, not yet substituted with numbers.
- Parameters:
Fx (MutableDenseMatrix)
Fz (MutableDenseMatrix)
Fu (MutableDenseMatrix)
Gx (MutableDenseMatrix)
Gz (MutableDenseMatrix)
Gu (MutableDenseMatrix)
Hx (MutableDenseMatrix)
Hz (MutableDenseMatrix)
Hu (MutableDenseMatrix)
n_us (int)
n_ug (int)
n_out_s (int)
n_out_g (int)
state_syms (list[Symbol])
alg_syms (list[Symbol])
input_syms (list[Symbol])
diffeq_exprs (list[Expr])
algeq_exprs (list[Expr])
output_exprs (list[Expr])
_cache (dict)
- Fx: MutableDenseMatrix
- Fz: MutableDenseMatrix
- Fu: MutableDenseMatrix
- Gx: MutableDenseMatrix
- Gz: MutableDenseMatrix
- Gu: MutableDenseMatrix
- Hx: MutableDenseMatrix
- Hz: MutableDenseMatrix
- Hu: MutableDenseMatrix
- param_syms()[source]
Every free symbol in the equations that isn’t a state/alg/input — i.e. the physical parameters (
wb,Ra,Lf, …). Sorted by name for a stable, reproducible ordering.- Return type:
list[Symbol]
- point_from_subs(subs)[source]
Pulls
(x0, z0, u0, p0)numeric arrays — in the exact symbol ordernonlinear_funcs()’s callables expect — out of the same substitution dictlinearize()takes. Lets callers reuse one dict to get both the linearized and the nonlinear operating point.
- nonlinear_funcs()[source]
Lambdify
diffeqVec/algeqVec/outputeqVecinto numeric callables. Not cached here (sympyMatrixisn’t reliably hashable) — component modules wrap this in their own@lru_cache``d accessor, the same way ``sm_dae()etc. are cached.- Return type:
- nonlinear_jacobians()[source]
Lambdify
Gz/Gx/Gu/Hz/Hx/Hu— the same Jacobianslinearize()uses, but as numeric(x, z, u, p) -> matrixcallables instead of being substituted with one fixed operating point. This is what a Newton solver needs for the nonlinear coupled algebraic system (seetimedomain/emt.py): unlikelinearize(), the Newton solve has to evaluate the Jacobian at whatever point the solver is currently at, not just once at an operating point.- Return type:
- cached_jacobians()[source]
nonlinear_jacobians(), built once per DAE.The component modules already cache each DAE, so caching the lambdified Jacobians on it means the (slow) lambdify happens once per model variant for the life of the process.
- Return type:
- linearize(subs)[source]
Substitute numeric parameter + operating-point values and eliminate z.
Evaluated through the lambdified Jacobians rather than by symbolic substitution. The two agree to ~1e-22 – they are the same expressions – but
Matrix.subson expressions this size is expensive enough to dominate everything built on it: measured on one synchronous machine, 952 ms against 0.74 ms, and a network’s linearisation calls this once per component.- Parameters:
subs (dict)
- Return type:
- g2elin_core.components.base.build_dae(*, state_vec, alg_vec, input_vec, diffeq_vec, algeq_vec, output_vec, n_us, n_ug, n_out_s, n_out_g, state_names, input_names, output_names)[source]
Jacobian bookkeeping shared by every
sym*port — mirrors theFx = jacobian(diffeqVec, stateVec)block repeated in every MATLAB file.- Parameters:
- Return type:
- g2elin_core.components.base.eq_symbol(sym)[source]
The
<name>_0equilibrium-point symbol forsym, matching the MATLAB toolbox’ssym(strcat(char(s),'_0'))convention.- Parameters:
sym (Symbol)
- Return type:
Symbol
- g2elin_core.components.base.mode_key(modes)[source]
A hashable, canonical form of a mode map, for the
lru_cacheon each component’s*_dae()builder.Dynamic entries are dropped, so a fully-dynamic model hashes to
()and shares its cache entry with a call that passed no modes at all – the full-order DAE is built once however it was asked for.
- g2elin_core.components.base.apply_reduction(*, states, modes, alg_vec, algeq_vec, output_vec)[source]
Lower a component’s model order by reclassifying individual states.
statesis the component’s full-order(symbol, d/dt expression, display name)list in its canonical order;modesmaps a state symbol’s name to one ofDYNAMIC,ALGEBRAICorFROZEN(anything unnamed stays dynamic). Returns the six listsbuild_dae()takes, ready to pass straight through.The two reductions are mathematically different and are not interchangeable, which is why they’re separate modes rather than one “drop this state” flag:
ALGEBRAIC(residualization / singular perturbation) replacesdx/dt = exprwith0 = exprand movesxinto the algebraic vector. The state still takes whatever value the rest of the system demands of it, instantaneously. Valid for states much faster than the phenomena being studied – stator flux, filter currents, network branches, inner control loops.FROZEN(truncation) substitutesxby its equilibrium valuex_0everywhere and drops its equation entirely. The state simply doesn’t move. Valid for states much slower than the window of interest – the field flux in the classical machine model, whereE'is held constant.
Applying the fast-state treatment to a slow state (or the reverse) is the most common way a hand-rolled reduced model goes quietly wrong: a field flux with
dpsi_fd/dt = 0is an instantaneous field winding, the exact opposite of the classical model’s constant one.A frozen state’s
x_0becomes an ordinary free symbol, soComponentDAE.param_syms()picks it up as a parameter andComponentDAE.point_from_subs()reads its value from the same operating-point substitution dict everything else comes from – which is why reduction needs no new initialization code anywhere.
- g2elin_core.components.base.equilibrium_subs(subs)[source]
subsplus anx_0entry for every symbol in it – the valuesFROZENstates are substituted by.Components call this on their own operating-point dict so that freezing any state works without the dict having to know which ones were frozen. The extra entries are harmless when nothing is frozen:
param_symsonly returns symbols that actually appear in the equations.
- class g2elin_core.components.base.RebuiltWithParams[source]
Bases:
objectMixin: an operating point that can be rebuilt with other parameters.
An operating point is built from the solved power flow – terminal voltage, dispatch, reference angle – plus the unit’s parameter set. Changing a machine constant leaves everything but the last of those alone, so the unit’s own operating point can be redone without redoing the network’s.
That matters for a parameter scan (
g2elin_core.modal.parameters), which perturbs one parameter at a time: rebuilding one unit is a fraction of a millisecond wherecompute_operating_pointon a 118-bus network is thirty.One unit is not always enough. In the MATLAB-compatible frame (
Network.frame_follows_slack) the slack machine’s rotor angle is the reference every other unit’s angle is measured from, so itsRa,LlandLaq– the three constants that set that angle – change every other unit’s operating point too. Rebuilding that one unit is then not the same as recomputing the network’s, and a caller that needs those right has to go the long way round; the parameter scan does.
Synchronous machine + transformer + AVR/PSS/governor, ported from
Functions/symSG.m (nonlinear DAE + linearization) and Functions/SG_subs.m
(numeric operating-point substitution).
Two known “use index 1 for all units” simplifications carry over unchanged
from the MATLAB source (script_generic.m), preserved here rather than
silently fixed, since fixing them would make results incomparable to a
MATLAB-generated reference:
Every machine’s transformer impedance (
Rt/Lt) comes from the first DG’sY_DERTR_R/TR_XL columns, not its own — seescript_generic.m’sSG_Dataconstruction (Y_TR(1,4) Y_TR(1,5)for every row i). No longer the default: each unit now uses its own transformer (the one the power flow uses, seeoperating_point.unit_transformer_rx); setNetwork.units_use_first_transformerto reproduce the MATLAB behaviour.Every node’s shunt susceptance (
Cl, ing2elin_core.components.node) comes from the first line’s B column, not the sum of its own incident lines’ — seescript_generic.m’s node-substitution loop (Cl = Y_line(1,6)).
- class g2elin_core.components.sm.ExciterKind(*values)[source]
-
Which AVR/exciter this machine carries.
See
docs/sphinx/design/controller-models.mdfor how these relate to the IEEE 421.5 model library.- G2ELIN = 'g2elin'
- KUNDUR = 'kundur'
- class g2elin_core.components.sm.PssKind(*values)[source]
-
Which power system stabiliser this machine carries, if any.
- G2ELIN = 'g2elin'
- KUNDUR = 'kundur'
- NONE = 'none'
No stabiliser at all – no states, and nothing added at the regulator’s summing junction.
- class g2elin_core.components.sm.GovernorKind(*values)[source]
-
Whether this machine’s prime mover responds to speed.
- G2ELIN = 'g2elin'
- NONE = 'none'
the mechanical power stays at whatever the operating point set, which is the “constant mechanical torque” assumption most textbook small-signal examples are worked under.
- Type:
No governor
- g2elin_core.components.sm.sm_dae(is_slack, modes=(), exciter=ExciterKind.G2ELIN, pss=PssKind.G2ELIN, governor=GovernorKind.G2ELIN)[source]
modesis amode_key()tuple naming the states this machine gives up – seeg2elin_core.reductionfor the catalogue and the named orders it builds out of them. The default()is the full model.exciterandpsschoose the regulators. The defaults are the models this component has always had, so the default machine is the same 19 states it always was;kunduron both gives the 16-state machine of Kundur Fig. E12.9 (three states fewer: the exciter loses its amplifier, exciter lag and rate feedback but gains a TGR state, and the stabiliser loses its input filter).- Parameters:
is_slack (bool)
exciter (ExciterKind)
pss (PssKind)
governor (GovernorKind)
- Return type:
- g2elin_core.components.sm.sm_nonlinear_funcs(is_slack, modes=(), exciter=ExciterKind.G2ELIN, pss=PssKind.G2ELIN, governor=GovernorKind.G2ELIN)[source]
Numeric callables for the full nonlinear SM DAE — see
g2elin_core.components.base.ComponentDAE.nonlinear_funcs().- Parameters:
is_slack (bool)
exciter (ExciterKind)
pss (PssKind)
governor (GovernorKind)
- Return type:
- g2elin_core.components.sm.sm_nonlinear_jacobians(is_slack, modes=(), exciter=ExciterKind.G2ELIN, pss=PssKind.G2ELIN, governor=GovernorKind.G2ELIN)[source]
Numeric Jacobian callables for the SM DAE’s algebraic/output equations — see
g2elin_core.components.base.ComponentDAE.nonlinear_jacobians().- Parameters:
is_slack (bool)
exciter (ExciterKind)
pss (PssKind)
governor (GovernorKind)
- Return type:
- class g2elin_core.components.sm.SmOperatingPoint(*, params, v_terminal_pu, angle_terminal_rad, p_terminal_pu, q_terminal_pu, v_grid_pu, angle_grid_rad, p_ref_pu, theta_g_rad, is_slack, exciter=ExciterKind.G2ELIN, pss=PssKind.G2ELIN, governor=GovernorKind.G2ELIN)[source]
Bases:
RebuiltWithParamsEverything needed to linearize one SM instance, computed from the solved power flow the way
script_generic.m’s “SM Initializations” section +SG_subs.mdo.
- g2elin_core.components.sm.EXCITER_SYMBOLS: dict[str, tuple[Symbol, ...]] = {ExciterKind.G2ELIN: (Tr, Ka, Ta, Ke, Te, Kfd, Tfd), ExciterKind.KUNDUR: (TR, KA, TA, TB)}
The parameter symbols each regulator model owns. The symbol name is the key in the unit’s parameter dict, so a model’s parameters are spelled the same in the equations, in the stored network and in the editor.
Kept per slot rather than in one dict: these enums are
strenums, soExciterKind.G2ELINandPssKind.G2ELINare the same dict key.
- g2elin_core.components.sm.EXCITER_GAIN_KEY = {ExciterKind.G2ELIN: 'Ka', ExciterKind.KUNDUR: 'KA'}
Each exciter’s regulator gain, whatever it is called in that model. The reference voltage is solved from it at initialisation – V_ref is whatever makes the regulator ask for the field voltage the power flow implies.
- g2elin_core.components.sm.EXCITER_PARAM_NAMES = {ExciterKind.G2ELIN: ('Tr', 'Ka', 'Ta', 'Ke', 'Te', 'Kfd', 'Tfd'), ExciterKind.KUNDUR: ('TR', 'KA', 'TA', 'TB')}
{model id: (parameter name, ...)}for whoever needs the names alone.
- g2elin_core.components.sm.linearize_sm(op, modes=None)[source]
- Parameters:
op (SmOperatingPoint)
- Return type:
- g2elin_core.components.sm.sm_nonlinear_point(op, modes=None)[source]
(x0, z0, u0, p0)forsm_nonlinear_funcs(), at the same operating pointlinearize_sm()uses.- Parameters:
op (SmOperatingPoint)
- Return type:
Grid-forming converter, ported from Functions/symGFM_types.m +
Functions/GFM_subs.m.
All five of that file’s outer power-control laws are here – droop, droop
behind a filter, dVOC, VSM and matching control – selected per unit through
DerUnit.controller. They differ only in the outer loop (see
_outer_loop()); the cascaded voltage and current loops, the LC filter
and the DC link are shared.
Only the non-slack variant and the Droop outer-loop are implemented —
CIGRE’s islanded preset uses GFM_P_control = 'Droop' and its GFM units
are never the slack. Droop+filter/Droop+PLL/dVOC/VSM/
Matching and the slack (i_DG == 1) branch follow the exact same
build_dae/linearize pattern (see symGFM_types.m’s other switch
cases) and would extend gfm_dae/GfmOperatingPoint the same way
sm.py handles is_slack — not done here to keep scope to what
CIGRE’s preset actually needs.
- class g2elin_core.components.gfm.GfmControlKind(*values)[source]
-
Which outer power-control law a grid-forming converter runs.
These are
symGFM_types.m’sP_typecases. They differ in three things and nothing else: which states the outer loop carries, how it forms the frequency deviationDw, and how it forms the voltage reference the cascaded loops then track. Everything downstream – the voltage loop, the current loop, the filter, the DC link – is identical.- DROOP = 'droop'
- DROOP_FILTERED = 'droop_filtered'
- DVOC = 'dvoc'
- VSM = 'vsm'
- MATCHING = 'matching'
- g2elin_core.components.gfm.OUTER_SYMBOLS: dict[str, tuple[Symbol, ...]] = {GfmControlKind.DROOP: (mp, nq, wf), GfmControlKind.DROOP_FILTERED: (mp, nq, wf, wc), GfmControlKind.DVOC: (eta, alfa, wf), GfmControlKind.MATCHING: (K_theta, wf), GfmControlKind.VSM: (J, Dp, K, Dq)}
The parameters each control law owns, under symGFM_types.m’s own names. A converter runs one law, so only its parameters are in its dict.
- g2elin_core.components.gfm.gfm_dae(modes=(), controller=GfmControlKind.DROOP)[source]
modesis amode_key()tuple naming the states this converter gives up – seeg2elin_core.reduction.controllerchooses the outer power-control law. The default is plain droop, the 15-state model this component has always built. The others carry different outer states: droop-behind-a-filter and dVOC add one to droop’s three, VSM and matching drop the power filters entirely for two and three states respectively.- Parameters:
controller (GfmControlKind)
- Return type:
- g2elin_core.components.gfm.gfm_nonlinear_funcs(modes=(), controller=GfmControlKind.DROOP)[source]
- Parameters:
controller (GfmControlKind)
- Return type:
- g2elin_core.components.gfm.gfm_nonlinear_jacobians(modes=(), controller=GfmControlKind.DROOP)[source]
- Parameters:
controller (GfmControlKind)
- Return type:
- class g2elin_core.components.gfm.GfmOperatingPoint(*, params, v_terminal_pu, angle_terminal_rad, p_terminal_pu, q_terminal_pu, v_grid_pu, angle_grid_rad, theta_g_rad, controller=GfmControlKind.DROOP)[source]
Bases:
RebuiltWithParamsMirrors
GFM_subs.m+ the “GFM Initializations” block ofscript_generic.m.
- g2elin_core.components.gfm.linearize_gfm(op, modes=None)[source]
- Parameters:
op (GfmOperatingPoint)
- Return type:
- g2elin_core.components.gfm.gfm_nonlinear_point(op, modes=None)[source]
(x0, z0, u0, p0)forgfm_nonlinear_funcs().- Parameters:
op (GfmOperatingPoint)
- Return type:
Grid-following converter (LCL filter + PLL), ported from
Functions/symGFL.m + Functions/GFL_subs.m. A GFL is never the
slack DG, so unlike SM/GFM there’s only one variant.
- g2elin_core.components.gfl.gfl_dae(modes=())[source]
modesis amode_key()tuple naming the states this converter gives up – seeg2elin_core.reduction. The default()is the full 14-state model.- Parameters:
- Return type:
- class g2elin_core.components.gfl.GflOperatingPoint(*, params, v_terminal_pu, angle_terminal_rad, p_terminal_pu, q_terminal_pu, v_grid_pu, angle_grid_rad, theta_g_rad)[source]
Bases:
RebuiltWithParamsMirrors
GFL_subs.m. Eg0/Ig0/Is0/Vm0 physics are identical tog2elin_core.components.gfm.GfmOperatingPoint(same LCL-filter converter structure) — see there for the derivation.M_d0/M_q0/M_CLd0/M_CLq0/M_pll0are derived from the inner-loop integrators’ own equilibrium conditions (dM_x = 0 at the operating point), sinceGFL_subs.mreads them from data columns this port doesn’t have an upstream initializer for; the resulting formulas match GFM’s structurally-identical ones exactly, as a cross-check.
- g2elin_core.components.gfl.linearize_gfl(op, modes=None)[source]
- Parameters:
op (GflOperatingPoint)
- Return type:
- g2elin_core.components.gfl.gfl_nonlinear_point(op, modes=None)[source]
(x0, z0, u0, p0)forgfl_nonlinear_funcs().- Parameters:
op (GflOperatingPoint)
- Return type:
Upstream infinite bus, ported from Functions/symIB.m + IB_subs.m.
The original toolbox made the IB the slack DG and the reference frame both
(per Functions/network_form.m: “an Infinite bus can never be a normal
DG, it is always assumed to be the slack DG”) – that is is_slack=True
here: the source sits at angle 0 of a frame turning with its own speed, and
the block hands that angle and speed out for the rest of the network to use.
is_slack=False is the same source in a frame it does not own (see
components/frame.py): it reads the frame angle theta_g and speed
wg like any other unit, and its source voltage enters at
theta_up - theta_g. The two coincide whenever the frame turns at the
source’s own speed, which is the default – what it buys is that the
infinite bus is no longer load-bearing for the model: it can be
disconnected, leaving the rest of the network islanded.
- g2elin_core.components.ib.linearize_ib(*, wb_val, r_pu, x_pu, v_pu, p_mw, q_mvar, sn_mva, delta_rad=0.0, is_slack=True)[source]
Linearize the infinite bus at its power-flow operating point.
r_pu/x_puare the DG’s own interconnecting-transformer impedance (Y_DERTR_R/TR_XL, in the slot the toolbox callsRup/Lup).p_mw/q_mvarare the power flowing out of the IB at its own bus,v_puits voltage magnitude there (angle is the local reference, 0).
- g2elin_core.components.ib.ib_nonlinear_funcs(is_slack=True)[source]
- Parameters:
is_slack (bool)
- Return type:
- g2elin_core.components.ib.ib_nonlinear_jacobians(is_slack=True)[source]
- Parameters:
is_slack (bool)
- Return type:
- g2elin_core.components.ib.ib_nonlinear_point(*, wb_val, r_pu, x_pu, v_pu, p_mw, q_mvar, sn_mva, delta_rad=0.0, is_slack=True)[source]
The common dq reference frame the whole network model is written in.
Every node, line and load equation uses the frame’s speed wg (the
rotational terms wg*L*i, wg*C*v), and every unit transforms between
its own dq frame and the common one through theta - theta_g. Which
frame that is, is a modelling choice, not physics: any frame gives the same
physical trajectories, expressed differently.
This component is the frame itself: one state, the frame angle, with no electrical connection to anything, turning either at a fixed speed
dtheta_f/dt = wb * w_ref, outputs: (theta_f, w_ref)
or with a machine it follows (driven=True), which is the usual case:
dtheta_f/dt = wb * w_in, outputs: (theta_f, w_in)
Either way it feeds exactly the theta/wr ports a slack unit used to
feed (see interconnect/network_assembly.py). The fixed variant is the
angle part an infinite bus already carries (components/ib.py:
dtheta_up = wb*wup with wup at 1 pu) without the voltage source
behind it – which is why every infinite-bus preset in this repository has
effectively been running on a frame of its own all along.
Why a block of its own: with the frame taken from the slack unit, that unit could never be disconnected – the frame would go with it. Standing apart, every unit is just a unit: any of them can be tripped, a network can be split into islands (one frame each, following that island’s own reference machine), and the slack keeps only its steady-state meaning, the power flow’s reference. EMT tools have no slack at all for the same reason.
Why it follows a machine rather than simply running at nominal speed. Both give identical trajectories – the nonlinear models agree to solver tolerance, since a frame is only a choice of coordinates. But governor and droop response leave the system at a slightly different frequency after a disturbance, and in a frame running at exactly nominal speed the network’s dq vectors then turn slowly forever: there is no steady state to linearise about, and modal analysis and the linearised overlay degrade the longer the horizon (measured on WSCC-9: 5.6 % error at 0.3 s, 17.9 % at 1.5 s, against 4.4 % and 5.1 % for a frame that follows the system). A frame carried by a machine settles with it, so the equilibrium exists and small-signal analysis stays sharp.
One free angle remains: nothing pins the frame’s absolute position, so a network with no infinite bus has a zero eigenvalue – the familiar reference-angle mode, a property of the formulation, not an instability.
- g2elin_core.components.frame.frame_dae(driven=True)[source]
driven: the frame turns with a machine’s speed (itsw_inport, wired to that machine’s own speed output); otherwise at the fixedw_ref, which is what an island referenced to an infinite bus needs.- Parameters:
driven (bool)
- Return type:
- g2elin_core.components.frame.frame_nonlinear_funcs(driven=True)[source]
- Parameters:
driven (bool)
- Return type:
- g2elin_core.components.frame.frame_nonlinear_jacobians(driven=True)[source]
- Parameters:
driven (bool)
- Return type:
- g2elin_core.components.frame.linearize_frame(*, wb_val, theta0, w0=1.0, driven=True)[source]
The frame as a linear block: an integrator of the speed it follows.
theta0is where the frame starts, i.e. the operating point’s own reference angle (NetworkOperatingPoint.theta_g_rad), so every unit’stheta - theta_gstarts at the value the operating point computed.- Parameters:
- Return type:
- g2elin_core.components.frame.frame_nonlinear_point(*, wb_val, theta0, w0=1.0, driven=True)[source]
(x0, z0, u0, p0)forframe_nonlinear_funcs().
RL-series transmission line, ported from Functions/symLine.m.
Also serves shunt reactors and branch transformers – both are the same RL
branch, differing only in how interconnect/network_assembly.py wires
their two ends (see that module).
With mode=ALGEBRAIC the branch’s L di/dt is dropped and the current
becomes an algebraic variable of the series impedance: the quasi-stationary
branch a phasor tool uses. Nothing else about the model changes – see
g2elin_core.components.base.apply_reduction().
- g2elin_core.components.line.line_dae(mode='dynamic', fixed_frequency=False)[source]
modeis this branch’s single state group – seeg2elin_core.reduction.fixed_frequencypins thew*Lspeed-voltage term to nominal instead of following the frame, which is the classic phasor-network convention.- Parameters:
- Return type:
- g2elin_core.components.line.line_nonlinear_funcs(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.line.line_nonlinear_jacobians(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.line.linearize_line(*, wb_val, r_pu, x_pu, wg0, ild_g0, ilq_g0, mode='dynamic', fixed_frequency=False)[source]
Linearize a line at an operating point.
x_puplays the role ofLlin(wb/Ll)*(...), matching the toolbox’s convention of using per-unit reactance directly in place of an inductance.vgdj_g/vgqj_g/vgdk_g/vgqk_g(the two end voltages) don’t appear in the linear system’s coefficients (onlywgdoes, since it multiplies the states), so they’re left at the_line_operating_subs()default of 0 here — harmless for linearization, but seeline_nonlinear_point()for where the real values are needed (a nonlinear equilibrium check needs the true voltages).
- g2elin_core.components.line.line_nonlinear_point(*, wb_val, r_pu, x_pu, wg0, ild_g0, ilq_g0, vgdj_g0, vgqj_g0, vgdk_g0, vgqk_g0, mode='dynamic', fixed_frequency=False)[source]
(x0, z0, u0, p0)forline_nonlinear_funcs(), at a line’s true operating point (unlikelinearize_line(), the end voltages matter here).
Network node (line-charging capacitance), ported from Functions/symNode.m.
With mode=ALGEBRAIC the bus’s C dv/dt is dropped, leaving
0 = i_sh + w*C*J*v: the injected current equals the shunt’s own
steady-state current, which is the bus row of a phasor tool’s admittance
matrix. The capacitance does not disappear from the model – it is still
the bus’s shunt admittance – it simply stops being integrated.
See g2elin_core.components.base.apply_reduction().
- g2elin_core.components.node.node_dae(mode='dynamic', fixed_frequency=False)[source]
modeis this bus’s single state group – seeg2elin_core.reduction.fixed_frequencypins thew*Cterm to nominal instead of following the frame.- Parameters:
- Return type:
- g2elin_core.components.node.node_nonlinear_funcs(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.node.node_nonlinear_jacobians(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.node.linearize_node(*, wb_val, b_pu, wg0, vgd_g0, vgq_g0, mode='dynamic', fixed_frequency=False)[source]
Linearize a node’s shunt (line-charging) capacitance at an operating point.
b_pu(total line-charging susceptance seen at this node) plays the role ofClin(wb/Cl)*(...).
- g2elin_core.components.node.node_nonlinear_point(*, wb_val, b_pu, wg0, vgd_g0, vgq_g0, mode='dynamic', fixed_frequency=False)[source]
(x0, z0, u0, p0)fornode_nonlinear_funcs().
Constant-impedance (RL) load branch, ported from Functions/symLoad.m.
With mode=ALGEBRAIC the branch’s L di/dt is dropped and the load
draws the current its own impedance demands at the bus voltage, instantly –
the constant-impedance load of a phasor tool.
See g2elin_core.components.base.apply_reduction().
- g2elin_core.components.load.load_dae(mode='dynamic', fixed_frequency=False)[source]
modeis this load’s single state group – seeg2elin_core.reduction.fixed_frequencypins thew*Lterm to nominal instead of following the frame.- Parameters:
- Return type:
- g2elin_core.components.load.load_nonlinear_funcs(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.load.load_nonlinear_jacobians(mode='dynamic', fixed_frequency=False)[source]
- Parameters:
- Return type:
- g2elin_core.components.load.linearize_load(*, wb_val, r_pu, x_pu, wg0, vgd_g0, vgq_g0, mode='dynamic', fixed_frequency=False)[source]
Linearize a constant-impedance load at an operating point.
x_pu(the load’s equivalent reactance,V_pu^2/S_pu * sin(acos(PF))) plays the role ofLc, matching the toolbox’s per-unit convention.
- g2elin_core.components.load.load_nonlinear_point(*, wb_val, r_pu, x_pu, wg0, vgd_g0, vgq_g0, mode='dynamic', fixed_frequency=False)[source]
(x0, z0, u0, p0)forload_nonlinear_funcs().