pu_base and operating_point
Two small modules that sit between the solved power flow and the symbolic
component models: pu_base computes the physical base
values a per-unit system needs, and operating_point turns bus voltages/
angles/powers into the exact operating-point arguments each component’s
*OperatingPoint class expects.
pu_base — amplitude-invariant per-unit bases
Ported from Functions/PU_calc.m. Note the AC bases use an
amplitude-invariant (peak-quantity) convention — Ub = sqrt(2/3)*Ul,
Ib = sqrt(2)*Il — rather than the more common RMS convention, matching the
EMT-oriented per-unit system the MATLAB toolbox uses throughout. The
resulting impedance base still reduces to the familiar
Un_kV**2 / Sn_MVA, which is what actually matters for the R/X/B
per-unit conversions used elsewhere in this package.
flowchart LR
IN["wb, un_kv, sn_mva"] --> FN["pu_base()"]
FN --> OUT["PuBase<br/>ub, ib, zb, lb, cb<br/>ub_dc, ib_dc, zb_dc, cb_dc"]
operating_point — from PowerFlowResult to per-component operating points
Ports script_generic.m’s “SM Initializations” section (computing SM.Pt,
SM.Et, flux linkages, etc. from bus_sol) plus the node/line/load
operating-point loops in its “Substituting in the individual state spaces”
section. This is also where two MATLAB fidelity quirks are implemented
deliberately (not accidentally): every DER unit’s transformer impedance
comes from DG#1’s row (first_tr, used for every unit rather than each
unit’s own transformer), and every node’s shunt susceptance comes from
line #1’s b_pu (handled downstream in the node component, but the values
this module hands it are what make that visible).
flowchart TD
N["Network"] --> CO["compute_operating_point()"]
PF["PowerFlowResult"] --> CO
CO --> SLACK["1. Slack SM first<br/>defines theta_g_rad (global angle ref)"]
SLACK --> SM["2. Remaining SM units<br/>-> SmOperatingPoint"]
SLACK --> GFMGFL["3. GFM / GFL units<br/>-> GfmOperatingPoint / GflOperatingPoint"]
SLACK --> NODES["4. Plain nodes<br/>-> node_vg (rotated to global frame)"]
SLACK --> LINES["5. Lines<br/>solve steady-state 2x2 -> line_i0"]
SLACK --> LOADS["6. Loads<br/>constant-impedance equivalent -> load_rx"]
SM --> RESULT["NetworkOperatingPoint"]
GFMGFL --> RESULT
NODES --> RESULT
LINES --> RESULT
LOADS --> RESULT
sm_params(), gfm_params() and gfl_params() hold the default
electrical/control/AVR/PSS parameter values transcribed directly from
script_generic.m’s “Defining Default Tuning” section — the same values
every preset case in the MATLAB source uses (none of the example scripts
override them), so hard-coding them here is a faithful port, not a
simplification. One exception: gfm_params()’s tr_cl (the inner current
loop’s closed-loop rise-time tuning target, Inner_current_loop.tr in the
MATLAB source) is an optional keyword, default unchanged from the fixed
value every preset used before — added so a caller can retune just that
one loop and trace how it moves the closed-loop eigenvalues, without
duplicating the pole-placement formula outside this function (see
notebooks/tour.ipynb’s root-locus section).
Physics
Per-unit bases
Given \(\omega_b = 2\pi f_n\), a bus’s nominal line-to-line voltage \(U_{n}\) (kV) and the network’s own MVA base \(S_n\):
The \(\sqrt{2/3}\)/\(\sqrt2\) factors make this an amplitude-invariant
(peak-quantity) base — the dq-frame convention used throughout every
component’s equations — rather than the more common RMS convention; the
impedance base \(Z_b\) still reduces to the familiar \(U_{n}^2/S_n\) (the
amplitude factors cancel), which is what the R/X/B per-unit values on
every Line/Transformer are already expressed against. A converter’s
DC-link base scales off the same AC quantities (a two-level VSC’s DC bus
voltage is nominally \(2U_b\) at unity modulation):
Default synchronous-machine parameters
Transcribed from script_generic.m’s “Defining Default Tuning” section —
every WSCC/CIGRE preset in this codebase uses these values unmodified.
Electrical (pu) |
Value |
Electrical (pu) |
Value |
|
|---|---|---|---|---|
\(R_a\) |
0.003 |
\(L_{1q}\) |
0.7252 |
|
\(L_l\) |
0.15 |
\(R_{1q}\) |
0.00619 |
|
\(L_{ad}\) |
1.66 |
\(L_{2q}\) |
0.125 |
|
\(L_{aq}\) |
1.61 |
\(R_{2q}\) |
0.02368 |
|
\(L_{fd}\) |
0.165 |
\(R_g\) (aux load, \(=S_n/P_L\)) |
1000 |
|
\(R_{fd}\) |
0.0006 |
|||
\(L_{1d}\) |
0.1713 |
|||
\(R_{1d}\) |
0.0284 |
Mechanical |
Value |
Governor ( |
Value |
|---|---|---|---|
\(H\) (s) |
5.0 |
\(m_p\) (droop, pu) |
0.005 |
\(K_D\) |
0.0 |
\(T_G\) (s) |
0.2 |
The regulator half depends on which models the machine carries. Each
exciter and stabiliser brings its own parameters under its own names
(DerUnit.exciter / DerUnit.pss / DerUnit.governor; see
components), so a machine’s key set is a function of the
models fitted and not of its type. overridable_param_keys(kind, exciter=, pss=, governor=, controller=) — or unit_keys(der) for a caller holding a
unit — is what everything that asks “what may this unit override” goes
through.
Exciter |
Value |
Exciter |
Value |
|---|---|---|---|
\(T_r\) (s) |
0.02 |
\(T_R\) (s) |
0.01 |
\(K_a\) |
300.0 |
\(K_A\) |
200.0 |
\(T_a\) (s) |
0.001 |
\(T_A\) (s, TGR lead) |
1.0 |
\(K_e\) |
1.0 |
\(T_B\) (s, TGR lag) |
10.0 |
\(T_e\) (s) |
0.0001 |
||
\(K_{fd}\) |
0.001 |
||
\(T_{fd}\) (s) |
0.1 |
PSS |
Value |
PSS |
Value |
|---|---|---|---|
\(K_{PSS}\) |
2.0 |
\(K_{STAB}\) |
20.0 |
\(T_{LP}\) (s) |
0.03 |
\(T_W\) (s) |
10.0 |
\(T_{HP}\) (s) |
2.0 |
\(T_1/T_2\) (s) |
0.05 / 0.02 |
\(T_{1n}/T_{1d}\) (s) |
0.05 / 0.02 |
\(T_3/T_4\) (s) |
3.0 / 5.4 |
\(T_{2n}/T_{2d}\) (s) |
3.0 / 5.4 |
The kundur values are the book’s own, from the data for Example 12.9. Note
that the book pairs its stabiliser with high transient gain — case (iv),
\(T_A = T_B\), TGR switched off — while the TGR values above are case (iii).
The two are alternatives, not a combination.
\(K_e\) is not IEEE’s \(K_E\). In IEEE DC-exciter models the exciter equation is \(T_E \dot E_{fd} = V_R - (K_E + S_E)E_{fd}\), with \(K_E\) multiplying the output. Here it multiplies the input. They coincide at \(K_e = 1\), which is the default, but a value taken off a datasheet will not mean what it says.
GFM/GFL loop-tuning: rise-time pole placement
Every PI loop in gfm_params()/gfl_params() is tuned the same way
(script_generic.m’s “Inner_current_loop”/”Outer_…” blocks): treat
the loop’s own plant as first-order (\(\frac{1}{s\tau + 1}\)-shaped after
normalizing by its own DC gain), pick a target rise time \(t_r\) and a
fixed damping ratio \(\zeta = 0.707\), and back out the natural frequency
and PI gains that place the closed loop there:
where \(\tau\) is the plant’s own time constant and \(g\) its DC gain. Every loop below is this same formula with different \((\tau, g, t_r)\):
Loop |
Plant \(\tau\) |
Plant gain \(g\) |
\(t_r\) |
Result |
|---|---|---|---|---|
GFM current loop |
\(L_1/(\omega_b R_1)\) |
\(1/R_1\) |
0.1 ms |
\(K_{pCL}, K_{iCL}\) |
GFM voltage loop |
— (\(g=\omega_b/C_f\), integrator plant) |
\(\omega_b/C_f\) |
15 ms |
\(K_{pVL}, K_{iVL}\) |
GFM DC-link voltage |
\(C_{dc}/(\omega_b G_{dc})\) |
\(1/G_{dc}\) |
5 ms |
\(K_{pdc}\) (I-only: \(K_{pdc}=\left(\tfrac{3\tau_{dc}}{t_{r}}-1\right)/g\)) |
GFL current loop |
\(L_1/(\omega_b R_1)\) |
\(1/R_1\) |
10 ms |
\(K_{pCL}, K_{iCL}\) |
GFL DC-link voltage |
\(C_{dc}/(\omega_b G_{dc})\) |
\(-1/G_{dc}\) |
100 ms |
\(K_{pd}, K_{id}\) |
GFL reactive-power loop |
— (I-only) |
— |
100 ms |
\(K_{iq} = -3/t_r\) |
GFL PLL |
— (\(g=\omega_b\)) |
\(\omega_b\) |
50 ms |
\(K_{p,pll}, K_{i,pll}\) |
A converter’s outer law brings its own parameters the same way
(DerUnit.controller): droop has \(m_p, n_q, w_f\), droop_filtered adds
\(w_c\), dvoc has \(\eta, \alpha, w_f\), vsm has \(J, D_p, K, D_q\) and
matching has \(K_\theta, w_f\). Every one is derived from the droop tuning
so the laws stay comparable — see components.
Equivalent tuning, and swapping a law
Because every law’s gains are written in terms of the same \(m_p\), \(n_q\) and
\(w_f\), every law’s gains can be read back as those three —
gfm_outer_tuning(params, controller) — together with the equivalent
inertia \(H = 1/(2 m_p w_f)\) they imply.
gfm_retuned(params, from_controller, to_controller=None, tuning=None) is
the pair of things that follow from that, and is what the editor calls:
Swapping a law carries the tuning across, so a converter tuned away from the defaults keeps its inertia and reactive gain instead of silently reverting. The mapping is invertible, so swapping back returns the original numbers.
Editing the tuning rebuilds whichever law is running from the new numbers. That is what lets \(m_p\), \(H\) and \(w_f\) be set on a VSM or a matching converter, which carry no parameter by those names at all — a VSM’s \(H\) is \(J/2\), and setting it moves \(J\) and \(K\).
\(H\) and \(w_f\) say the same thing twice, so when both are given \(H\) wins: it is the physical quantity, and the one a comparison between laws is usually held fixed at.
Matching control is the one law that cannot report everything: it carries no reactive gain of its own, so \(n_q\) comes back as the default rather than as something inferred from nothing.
The droop coefficients themselves are fixed, not pole-placed: \(m_p = 1/K_{D,opt}\) with \(K_{D,opt}=200\) (so \(m_p = 0.005\) pu, matching the SM’s own \(m_p\) above — deliberately, so a GFM’s droop response is comparable to an SM’s governor droop at the same operating point), \(n_q = 10^{-4}\) pu, and the power-measurement filter \(\omega_f = \frac{1}{2 m_p H_{1st}}\) with a first-order-equivalent inertia \(H_{1st}=3.0\,\mathrm s\).
From a solved power flow to a DER’s own operating point
Given a DER’s terminal voltage phasor \(\bar V_0 = V_t\,e^{j\theta_t}\) (from
the power-flow bus solution) and its gross injected complex power \(\bar
S_0 = P_0 + jQ_0\) (net power-flow injection plus the unit’s own
p_cons_mw/q_cons_mvar), every operating-point class starts from the
same terminal current:
Synchronous machine — the classical “\(E_q\) behind \(X_q\)” angle-finding step locates the rotor’s own \(d\)-axis before any dq rotation is possible, using the machine’s own stator resistance \(R_a\) and \(q\)-axis synchronous reactance \(L_q = L_l + L_{aq}\):
Rotating \(\bar V_0\), \(\bar I_0\), and the current split off the machine’s own small aux load (\(\bar I_{c,0}=\bar V_0/R_g\), \(\bar I_{g,0} = \bar I_0-\bar I_{c,0}\)) into the machine’s own dq frame by \(e^{-j\theta_0}\) gives \(v_{ed0}, v_{eq0}, i_{d0}, i_{q0}, i_{gd0}, i_{gq0}\) directly, and the flux linkages/field current follow from the stator voltage equations at steady state (\(\dot\psi \equiv 0\)):
SmOperatingPoint cross-checks \(i_{d0}\)/\(i_{q0}\)/\(i_{fd0}\) against an
independent second derivation — solving the same flux/current matrix
relation components’s algebraic equations use — and
raises immediately if they disagree by more than float tolerance; this
is what caught the double-rotation bug documented in
components’s own page.
GFM/GFL — no rotor angle to locate first; the unit’s own frame angle is the terminal-voltage angle directly, \(\theta_0=\theta_t\). The filter capacitor’s charging current and the converter-side (behind \(R_f/L_f\)) voltage follow directly:
then every dq quantity is \(e^{-j\theta_0}\) of its own phasor above,
exactly the same physics for both unit types (a GFL’s PI integrator
states \(M_d, M_q, M_{CLd}, M_{CLq}, M_{pll}\) are then back-solved from
each loop’s own steady-state condition \(\dot M \equiv 0\), since
GFL_subs.m reads them from upstream data columns this port has no
equivalent source for — see GflOperatingPoint’s own docstring).
Grid-side node/line/load operating points are simpler still: a plain bus’s dq voltage is just its power-flow \((V,\theta)\) rotated into the global frame by \(e^{-j\theta_g}\) (no per-unit rotor angle involved); a line’s current solves its own steady-state \(2\times2\) Ohm’s-law system directly from its two end voltages; a load’s equivalent series impedance is \(Z=V^2/S\) at angle \(\arccos(P/S)\) (inductive convention, \(Q\ge0\)).
Per-unit rebasing: DerUnit.sn_mva
Published machine data is per unit of the machine’s rating, not the
network’s. A unit may therefore declare its own sn_mva, which states the
base its params overrides are given on; they are converted to the network
base before use (rebase_params). With \(Z_{base} = V^2/S\):
scales by |
example (900 MVA machine, 100 MVA network) |
|
|---|---|---|
impedances, reactances, droops |
\(S_{net}/S_{unit}\) |
\(x_d = 1.8 \to 0.2\) pu |
inertia, damping, capacitances |
\(S_{unit}/S_{net}\) |
\(H = 6.5 \to 58.5\) s |
Gains and time constants are tuning rather than machine data – a rise time
is a rise time on any base, and a gain’s base depends on which signals it sits
between – so they are taken as given, and validate_network warns when one
is passed to a rated unit. A rated unit’s model is bit-identical to the same
machine converted by hand.
Reference
Per-unit base-value calculator, ported from Functions/PU_calc.m.
Note the AC bases use an amplitude-invariant (peak-quantity) convention —
Ub = sqrt(2/3)*Ul, Ib = sqrt(2)*Il — rather than the more common
RMS convention, matching the EMT-oriented per-unit system the MATLAB
toolbox uses throughout. The resulting impedance base Zb = Ub/Ib still
reduces to the familiar Un_kV^2 / Sn_MVA (the amplitude factors
cancel), which is what actually matters for the R/X/B per-unit conversions
used elsewhere in this package.
- class g2elin_core.pu_base.PuBase(ub: 'float', ib: 'float', zb: 'float', lb: 'float', cb: 'float', ub_dc: 'float', ib_dc: 'float', zb_dc: 'float', cb_dc: 'float')[source]
Bases:
object- Parameters:
Bridges a solved PowerFlowResult to the
per-component operating points sym*_subs-equivalents need.
Ports script_generic.m’s “SM Initializations” section (computing
SM.Pt, SM.Et, flux linkages, etc. from bus_sol) plus the
node/line/load operating-point loops in its “Substituting in the individual
state spaces” section.
- g2elin_core.operating_point.sm_params(*, sn_mva, f_hz, rt_pu, lt_pu, exciter='g2elin', pss='g2elin', governor='g2elin')[source]
Full parameter dict for
g2elin_core.components.sm.linearize_sm().The regulator half depends on which models the machine carries: each one brings its own parameters under its own names, so what a user edits is the model’s own data sheet rather than a shared set of near-equivalents. A machine’s key set therefore depends on
exciterandpss, which is why everything that asks what a unit may override has to be told them – seeoverridable_param_keys().
- g2elin_core.operating_point.gfm_outer_tuning(params, controller, *, kpdc=None)[source]
The droop-equivalent tuning a law’s own parameters stand for.
The inverse of
_gfm_outer_params(). Every law’s gains are written in terms ofmp,nqandwf– that is what makes them comparable – so every law’s gains can be read back as those three, and a unit re-tuned or switched to another law without losing what it was tuned to.Hcomes out with them: the equivalent inertia is1/(2*mp*wf), which is the quantityscript_generic.mstarts from and the one worth holding fixed when comparing laws.A law that does not carry a quantity cannot report it: matching control has no reactive gain of its own, so
nqcomes back as the default rather than as something inferred from nothing.
- g2elin_core.operating_point.gfm_retuned(params, from_controller, to_controller=None, tuning=None)[source]
paramsre-expressed, for another control law or another tuning.Returns
(params, tuning)– the new parameter dict and the droop-equivalent tuning it stands for.This is what the editor calls for both of the things a user can do to a converter’s outer loop. Swapping the law carries the tuning across, so the unit keeps the inertia and reactive gain it had rather than silently reverting to the defaults. Editing the tuning rebuilds whichever law is running from the new numbers, so
mp,Handwfcan be set on a VSM or a matching converter that has no parameter by those names.Handwfsay the same thing twice (H = 1/(2*mp*wf)), so when both are givenHwins: it is the physical quantity, and the one a comparison between laws is usually held fixed at.
- g2elin_core.operating_point.gfm_params(*, sn_mva, f_hz, un_kv, rt_pu, lt_pu, tr_cl=0.0001, controller='droop')[source]
Full parameter dict for
g2elin_core.components.gfm.linearize_gfm().tr_clis the inner current loop’s closed-loop rise-time tuning target (script_generic.m’sInner_current_loop.tr) – the pole- placement formula below (wn_cl = 3/(zita*tr_cl), thenKpCL/KiCLfrom it) is transcribed unchanged from that file. Exposed as a parameter (default: the same fixed value every preset used before this) so a caller can retune just this one loop, e.g. to trace how it moves the closed-loop eigenvalues – seenotebooks/tour.ipynb.
- g2elin_core.operating_point.gfl_params(*, sn_mva, f_hz, un_kv, rt_pu, lt_pu)[source]
Full parameter dict for
g2elin_core.components.gfl.linearize_gfl().
- g2elin_core.operating_point.overridable_param_keys(unit_type, *, exciter=None, pss=None, governor=None, controller=None)[source]
Names a
DerUnit.paramsoverride may use forunit_type(empty for an infinite bus, which has no parameter set of its own).The key set depends on nothing but the type and, for a machine, its regulator models – each of which brings its own parameters under its own names – so any base values do. Callers holding a unit should pass
exciter/pss, or useunit_keys().
- g2elin_core.operating_point.unit_keys(der)[source]
Names
dermay override, for the regulator models it carries.
- g2elin_core.operating_point.unit_transformer_rx(network, der)[source]
(Rt, Lt)in system per unit forder’s dynamic model.By default the unit’s own transformer (the one whose LV side is the unit’s bus) – the same element the power flow uses – converted from per unit of the transformer’s rating to per unit of the network base (
Z_sys = Z_tr * Sn_sys / Sn_tr), so power flow and dynamics describe the same impedance. Withnetwork.units_use_first_transformer(the MATLAB tool’s convention, kept to reproduce its results), every unit uses the network’s first transformer’s values unconverted. Falls back to the first transformer when the unit has none of its own (such a network fails validation for modal/EMT anyway), and to placeholders when the network has no transformer at all.
- g2elin_core.operating_point.unit_params(network, der)[source]
The parameter dict
der’s model uses innetwork: its type’s defaults (with the models’ shared-transformer convention: the network’s first transformer) plusder.paramsoverrides. Empty for an infinite bus.
- g2elin_core.operating_point.rebase_params(params, *, from_mva, to_mva)[source]
paramsgiven per unit offrom_mva, expressed per unit ofto_mva.
- g2elin_core.operating_point.apply_param_overrides(der, params, network_sn_mva=None)[source]
paramswithder.paramsapplied on top; raises on an override name this unit type doesn’t have (validate_network() reports the same).When the unit declares its own rating (
DerUnit.sn_mva) its overrides are read per unit of that rating and converted to the network’s base first – seerebase_params().paramsitself (the type’s defaults) is already on the network base and is left alone.
- class g2elin_core.operating_point.NetworkOperatingPoint(theta_g_rad, sm_ops, gfm_ops, gfl_ops, ib_ops, node_vg, line_i0, load_rx, shunt_rx=<factory>, shunt_i0=<factory>, transformer_rx=<factory>, transformer_i0=<factory>)[source]
Bases:
objectEverything needed to linearize every component in a solved network.
- Parameters:
theta_g_rad (float)
sm_ops (dict[int, SmOperatingPoint])
gfm_ops (dict[int, GfmOperatingPoint])
gfl_ops (dict[int, GflOperatingPoint])
- sm_ops: dict[int, SmOperatingPoint]
- gfm_ops: dict[int, GfmOperatingPoint]
- gfl_ops: dict[int, GflOperatingPoint]
- g2elin_core.operating_point.compute_operating_point(network, result)[source]
- Parameters:
network (Network)
result (PowerFlowResult)
- Return type: