Excitation, stabiliser and governor models — a proposal
Status: the two Kundur Fig. E12.9 blocks of §4.1 are implemented, with the
book’s own parameter values, and the governor and stabiliser are optional
equipment (DerUnit.exciter / DerUnit.pss / DerUnit.governor, all
defaulting to the original models). Everything else here is still a
proposal. See §8 for what implementing them settled and what it changed
about the rest of the plan.
This is a plan for giving G2ELin a library of the standard AVR/exciter, PSS and turbine-governor models, instead of the single hard-wired set it has today. It says what exists now, what I propose to add and why, the equations and block diagram of each proposed model, and the four architectural decisions that have to be made before any of it is written.
It also answers the question that prompted it: what GENROU and GENCLS are, and how they relate to the machine model already here.
1. Recommendation in one table
Twelve models, in three phases. The column that matters is the last one:
every model in phase 1 and 2 exists in ANDES 2.0, which is already installed
in this repo’s environment and is already the cross-validation reference
(tools/compare_andes.py). Every one of them can therefore be checked
against an independent implementation rather than against my reading of a
standard.
Kind |
Model |
States |
Why this one |
ANDES |
|---|---|---|---|---|
Exciter |
|
2 |
The simplest realistic AVR. The sane default for a bus you have no data for. |
✅ |
|
4 |
Static (thyristor) excitation — the dominant modern technology, and the model in your Kundur figure. |
✅ |
|
|
4 |
DC commutator exciter with saturation. Legacy technology, but it is what most existing datasets contain. Closest to what G2ELin has now. |
✅ |
|
PSS |
|
5 |
The classical single-input stabiliser. A strict generalisation of G2ELin’s present PSS. |
— (see |
|
7 |
PSS1A plus a selectable input signal and a second-order lead-lag. Supersedes PSS1A if you only want one. |
✅ |
|
Governor |
|
2 |
The most-used simple steam governor. A strict generalisation of G2ELin’s present governor. |
✅ |
|
6 |
The most-used detailed steam governor: HP/IP/LP stages, optional cross-compound second shaft. |
✅ |
|
|
4 |
Hydro, with penstock water inertia. Gives the non-minimum-phase response no linear governor can fake. |
✅ |
|
|
3 |
Gas turbine, with the exhaust-temperature limit. |
✅ |
Phase 3, deliberately deferred — PSS2B (dual-input, integral-of-accelerating-power),
ESST4B / AC8B (digital PID exciters), GGOV1 (modern general-purpose
governor), PSS4B (multi-band). Rationale in §6. Note in particular that
ANDES 2.0 has no PSS2B, so it is the one popular model on this list with
no ready cross-validation partner — which is exactly why it should not be
first.
2. What G2ELin has today
Worth writing down precisely, because three of the proposed models turn out to be supersets of what is already here, which makes them cheap.
components/sm.py builds one fixed 19-state machine:
Block |
States |
Count |
|---|---|---|
Step-up transformer current |
|
2 |
Stator flux |
|
2 |
Rotor windings |
|
4 |
Swing |
|
2 |
Governor |
|
1 |
PSS |
|
4 |
AVR / exciter |
|
4 |
2.1 The governor as implemented
dP_m/dt = (P_ref − ΔP − P_m) / T_G , ΔP = (ω_r − ω_ref) / m_p
One state: a droop 1/m_p into a first-order lag T_G. No limits, no
turbine stages.
2.2 The PSS as implemented
dΔω_1/dt = (Δω_r − Δω_1) / T_LP input low-pass
dv_1/dt = K_PSS·(dΔω_1/dt) − v_1 / T_HP washout, gain folded in
dv_2/dt = (T_1n·(dv_1/dt) + v_1 − v_2) / T_1d lead-lag 1
dv_pss/dt= (T_2n·(dv_2/dt) + v_2 − v_pss) / T_2d lead-lag 2
Four states: an input filter, a washout and two lead-lags, driven by rotor speed deviation. This is the PSS1A structure with the second-order input filter degenerated to a single pole.
Note the idiom — a lead-lag (1+sT_n)/(1+sT_d) is written using the
derivative of the upstream state rather than by splitting out a
feedthrough term. It works because dv_1/dt is available symbolically, it
keeps every block strictly proper, and every new block below should follow
it.
2.3 The exciter as implemented
de_1/dt = (E_t − e_1) / T_r terminal voltage transducer
de_2/dt = (K_a·(V_ref − e_1 − e_3 + v_pss) − e_2) / T_a regulator
de_fd/dt = (K_e·e_2 − e_fd) / T_e exciter
de_3/dt = (K_fd·(de_fd/dt) − e_3) / T_fd rate feedback
Four states, with defaults Tr=0.02, Ka=300, Ta=0.001, Ke=1, Te=1e-4, Kfd=0.001, Tfd=0.1.
Two things to flag:
K_ehere is not IEEE’sK_E. In IEEE DC-exciter models the exciter equation isT_E·dE_fd/dt = V_R − (K_E + S_E(E_fd))·E_fd—K_Eis the self-excitation constant, multiplying the output. Here it multiplies the input. WithK_e = 1the two coincide, which is why the default is 1, but a user who setsKe = 0.3from a datasheet will get the wrong model silently. Any renaming of this parameter is a breaking change to saved networks, so it should happen in the same release as the model library.
With the shipped defaults this is already a static exciter.
T_e = 1e-4andT_a = 1e-3make both lags effectively instantaneous andK_fd = 0.001makes the rate feedback vestigial, so the model behaves as a high-gainK_a = 300proportional regulator — i.e. as ST1A, not as a DC exciter. The structure is DC-exciter-shaped; the defaults are static. AddingEXST1therefore mostly makes the existing behaviour honest.
2.4 What is missing, across all three
No limits anywhere. No
V_RMAX/V_RMIN, noV_MAX/V_MINon the valve, no PSS output clamp. Every real model in §4 has them.No saturation. No exciter
S_E, no machine saturation.No choice of input signal for the PSS (speed only).
No turbine. The governor output is the mechanical power; there is no steam chest, reheater or penstock.
3. Your question: GENROU and GENCLS
These are machine models, not controllers — PSS/E’s names, adopted by PowerWorld, ANDES and most of the industry.
3.1 What they are
GENCLS — the classical model. Two states, δ and ω, with a constant
voltage E∠δ behind a fixed transient reactance X'd. No field winding, no
dampers, so an AVR or PSS cannot be attached to it in any meaningful way. Used
for machines far from the disturbance, and for system equivalents.
GENROU — the round-rotor model, the workhorse for thermal units. Six
states: δ, ω, E'q, E'd, ψ''d, ψ''q. One field winding and one
damper on the d-axis; two windings on the q-axis. Quadratic saturation on the
air-gap flux. Its sibling GENSAL is the salient-pole version (no q-axis
transient winding, d-axis saturation only); GENTPF/GENTPJ are newer
variants with better saturation, GENTPJ’s depending on stator current too.
3.2 How they differ from the model you have
There are four differences, and only two of them are real.
(a) Parameter form — the big one. Your model is written in circuit
parameters: actual winding inductances and resistances, Lad, Laq, Lfd, Rfd, L1d, R1d, L1q, R1q, L2q, R2q, with the currents recovered by inverting a
3×3 inductance matrix per axis:
invd = Matrix([[-Lad-Ll, Lad, Lad], [-Lad, L1d+Lad, Lad], [-Lad, Lad, Lfd+Lad]]).inv()
GENROU is written in operational parameters — Xd, Xq, X'd, X'q, X''d, X''q, Xl, T'do, T'qo, T''do, T''qo — which is the form manufacturers publish
and every data file contains. The two are related by a standard but
assumption-laden conversion. This is the practical difference: your model
cannot ingest a PSS/E .dyr file without that conversion.
(b) Stator transients. Your full model keeps dψ_d/dt and dψ_q/dt, so
it represents the 50/60 Hz stator transient and the DC offset — it is an EMT
model. GENROU never has them; it is a phasor/RMS model by construction.
Your order6 level already removes them, which is the counterpart.
(c) The speed-voltage term. Your stator equations carry ω_r·ψ with the
actual rotor speed. GENROU (and most RMS tools) sets ω ≈ 1 there. Yours is
the more accurate choice; the difference is second-order in speed deviation
and only visible in large excursions.
(d) Saturation. GENROU has it, you do not. This is a genuine gap, and it
matters at full load and during faults, where the effective Xd drops
noticeably.
(e) Subtransient saliency. Classic PSS/E GENROU enforces X''d = X''q
(a modelling convenience, not physics). Your L1q/L2q/Laq are free, so
you do not have that restriction. ANDES parameterises xd2 and xq2
separately too.
3.3 The mapping
PSS/E |
G2ELin level |
Honest verdict |
|---|---|---|
|
|
Equivalent. Constant flux behind |
|
|
Structurally the same model. Same winding count, same neglect of stator transients. Differs by: parameter form, no saturation, |
|
|
Close. GENSAL’s defining assumption is |
— |
|
No PSS/E equivalent. Stator transients put this outside what any phasor tool represents; the comparison is with an EMT tool (PSCAD, EMTP, Simscape). |
So the short answer: you have already implemented GENROU, in a different parameter set, without saturation, and with an EMT mode on top that PSS/E does not have. The two things worth adding for real interoperability are the operational-parameter conversion and saturation — both separate from the controller work in this document.
4. The proposed models
Notation: E_t terminal voltage, V_ref reference, V_s stabiliser output,
E_fd field voltage, ω rotor speed pu, P_m mechanical power. Limits are
shown in the diagrams and listed per model; §5.1 covers how they should be
treated.
4.1 EXST1 / ST1A — static (thyristor) exciter · 4 states
This is the model in your figure. Potential-source, controlled-rectifier:
the exciter is a thyristor bridge fed from the generator terminals, so
there is no exciter time constant at all — the field voltage follows the
regulator directly, and the ceiling is proportional to E_t.
┌──────────┐
V_s ─►│ │
│ │ ┌───────────┐ ┌───────────┐
┌─────────┐ │ Σ │ │ 1 + sT_C │ │ K_A │
E_t ───►│ 1 │────┬─────────►│ − │──►│ ─────── │─►│ ─────── │──┬──► E_fd
│ ─────── │ │ │ │ │ 1 + sT_B │ │ 1 + sT_A │ │
│ 1 + sT_R│ │ V_ref ─►│ + │ └───────────┘ └───────────┘ │
└─────────┘ │ └──────────┘ lead-lag regulator │
transducer │ ▲ │
│ │ ┌───────────┐ │
│ └──────────│ sK_F │◄────────────────┘
│ │ ─────── │
│ │ 1 + sT_F │
│ └───────────┘
│ rate feedback
└──► limits: V_RMAX·E_t − K_C·I_fd (ceiling scales with E_t)
dv_1/dt = (E_t − v_1) / T_R
u = V_ref − v_1 − v_F + V_s (limited to V_IMIN..V_IMAX)
dv_2/dt = (u − v_2) / T_B
y = (T_C/T_B)·u + (1 − T_C/T_B)·v_2 lead-lag output
dv_R/dt = (K_A·y − v_R) / T_A
dv_F/dt = (K_F·(dv_R/dt) − v_F) / T_F
E_fd = v_R limited to V_RMIN..V_RMAX (×E_t, −K_C·I_fd)
Parameters: T_R, V_IMAX, V_IMIN, T_C, T_B, K_A, T_A, V_RMAX, V_RMIN, K_C, K_F, T_F.
Your Kundur figure as a special case
The figure is EXST1 with the regulator lag removed and no rate feedback:
E_t ──►[ 1/(1+sT_R) ]──┐
▼
V_ref ──►(Σ)──►[ K_A ]──►[ (1+sT_A)/(1+sT_B) ]──► E_fd
▲ TGR
V_s ─────┘
▲
Δω_r ─►[K_STAB]─►[ sT_W/(1+sT_W) ]─►[(1+sT_1)/(1+sT_2)]─►[(1+sT_3)/(1+sT_4)]
washout lead-lag 1 lead-lag 2
Two traps in this figure, both about names.
1.
T_AandT_Bmean different things than in IEEE ST1A. Kundur’s TGR block is(1+sT_A)/(1+sT_B); IEEE’s lead-lag is(1+sT_C)/(1+sT_B)and IEEE’sT_Ais the regulator lag, which Kundur’s figure does not have. Mapping:T_C := T_A^Kundur,T_B := T_B^Kundur,T_A^IEEE = 0,K_F = 0. If the parameter names are taken straight off the figure into an EXST1 implementation, the lead time constant lands in the regulator lag and the model is silently wrong. The catalogue should store IEEE names and offer the Kundur figure as a named preset that fills them.2. The summing-junction signs as drawn are positive feedback. The screenshot marks
E_twith+andV_refwith−, givingE_fd = K_A·(E_t − V_ref + V_s)·TGR. WithK_A > 0a rise in terminal voltage would raise field voltage. A regulator must be negative feedback, so the implementable form isE_fd = K_A·(V_ref − E_t + V_s)·TGR— which is what every other Kundur figure and IEEE 421.5 use. Worth confirming against the book before transcribing, but do not implement the signs as drawn.
TGR (transient gain reduction) is the point of the T_A/T_B block: with
T_B ≫ T_A the steady-state gain stays at K_A while the gain above
1/T_B falls to K_A·T_A/T_B. High DC gain gives tight voltage regulation;
low transient gain stops the AVR from destabilising the electromechanical
mode. A PSS is the alternative way to buy the same stability, which is why
the two appear together in this figure — and why this single figure is the
right first model to implement: it is the textbook demonstration of the
AVR-destabilises / PSS-restores interaction your eigenvalue map exists to
show.
4.2 SEXS — simplified excitation system · 2 states
┌──────────┐ ┌───────────┐ ┌──────────────┐
E_t ──►(Σ)─────►│ 1 + sT_A │──►│ K │──►│ E_MIN..E_MAX│──► E_fd
▲ − │ ──────── │ │ ─────── │ └──────────────┘
V_ref ──┘ + │ 1 + sT_B │ │ 1 + sT_E │ anti-windup
V_s ────┘ + └──────────┘ └───────────┘
lead-lag regulator
u = V_ref − E_t + V_s
dx_1/dt = (u − x_1) / T_B
y = (T_A/T_B)·u + (1 − T_A/T_B)·x_1
dE_fd/dt= (K·y − E_fd) / T_E anti-windup at E_MIN..E_MAX
Parameters: T_A/T_B (ANDES takes the ratio TATB and TB), K, T_E,
E_MIN, E_MAX. No terminal-voltage transducer lag — the measurement is
assumed instantaneous.
Two states, no saturation, no feedback loop. This is what to attach to the forty machines in a 118-bus case that you have no real data for; it is stable and well-behaved and does not invent detail.
4.3 EXDC2 / IEEET1 — DC commutator exciter · 4 states
┌─────────┐ ┌──────────┐ ┌───────────┐ ┌─────────┐
E_t ───►│ 1/(1+sT_R)│──►(Σ)►│ 1 + sT_C │─►│ K_A │─►(Σ)────►│ 1 │──┬─► E_fd
└─────────┘ ▲ − │ ──────── │ │ ─────── │ ▲ − │ ─────── │ │
│ │ 1 + sT_B │ │ 1 + sT_A │ │ │ sT_E │ │
V_ref ──►│ + └──────────┘ └───────────┘ │ └─────────┘ │
V_s ────►│ + V_RMIN..V_RMAX│ │
│ − │ │
│ (K_E + S_E(E_fd))·E_fd ◄───────┤
│ │
│ ┌───────────┐ │
└────────│ sK_F │◄────────────────────────────────┘
│ 1 + sT_F │
└───────────┘
dv_1/dt = (E_t − v_1) / T_R
u = V_ref − v_1 − v_F + V_s
dx_LL/dt = (u − x_LL) / T_B ; y = (T_C/T_B)·u + (1 − T_C/T_B)·x_LL
dv_R/dt = (K_A·y − v_R) / T_A anti-windup V_RMIN..V_RMAX
dE_fd/dt = (v_R − (K_E + S_E(E_fd))·E_fd) / T_E
dv_F/dt = (K_F·(dE_fd/dt) − v_F) / T_F
with quadratic saturation (ANDES ExcQuadSat, from the two published
points (E_1, S_E1) and (E_2, S_E2)):
S_E(x) = B·(x − A)² / x for x > A, 0 otherwise
Parameters: T_R, K_A, T_A, T_B, T_C, V_RMAX, V_RMIN, K_E, T_E, K_F, T_F, E_1, S_E1, E_2, S_E2.
This is the closest standard model to what G2ELin has today. Implementing it
converts the existing ad-hoc exciter into a named, documented, validatable
one, and the migration is mostly a parameter remapping — plus the K_e
correction in §2.3 and the saturation term, which is new.
S_E is smooth and differentiable everywhere except at x = A, so it can go
into the symbolic model directly and be linearised; no special treatment is
needed as long as the operating point is not sitting exactly on the breakpoint.
4.4 PSS1A — single-input stabiliser · 5 states
┌──────────────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
u ────►[ K_S ]►│ 1 │─►│ 1 + sT_1 │─►│ 1 + sT_3 │─►│ sT_W │──► V_s
│ ──────────────── │ │ ──────── │ │ ──────── │ │ ──────── │
│ 1 + sA_1 + s²A_2 │ │ 1 + sT_2 │ │ 1 + sT_4 │ │ 1 + sT_W │
└──────────────────┘ └───────────┘ └───────────┘ └──────────┘
torsional filter lead-lag 1 lead-lag 2 washout
V_SMIN..V_SMAX
G2ELin’s present PSS is this with 1/(1+sA_1+s²A_2) collapsed to
1/(1+sT_LP), and with the washout placed first rather than last (identical,
the chain is linear). So this is a two-state addition plus limits, not a
new model.
The second-order filter is what rejects the torsional modes of the
turbine-generator shaft (typically 10–50 Hz). A stabiliser with enough gain
to damp a 0.2–2 Hz electromechanical mode will otherwise excite them, which
is a real and destructive failure mode. A single pole cannot place a
complex-conjugate notch; A_1/A_2 can.
4.5 IEEEST — generalised single-input stabiliser · 7 states
PSS1A plus a selectable input signal and a second-order lead-lag. Worth having because the choice of input is itself a design question your tool is well placed to answer:
MODE |
Input signal |
|---|---|
1 |
rotor speed deviation |
2 |
bus frequency deviation |
3 |
generator electrical power |
4 |
accelerating power |
5 |
bus voltage magnitude |
6 |
bus voltage derivative |
u ─►[ 1/(1+sA_1+s²A_2) ]─►[ (1+sA_3+s²A_4)/(1+sA_5+s²A_6) ]
─►[ (1+sT_1)/(1+sT_2) ]─►[ (1+sT_3)/(1+sT_4) ]─►[ K_S ]
─►[ sT_5/(1+sT_6) ]─►[ L_SMIN..L_SMAX ]─► V_s (gated off outside V_CL..V_CU)
If you implement only one stabiliser, implement this one — PSS1A is the
special case with A_3..A_6 = 0.
4.6 TGOV1 — steam governor + turbine · 2 states
┌────────────┐ ┌───────────┐
ω−ω_ref ──►(Σ)─────►│ 1/R │──►│ 1 │──►│ 1 + sT_2 │──►(Σ)──► P_m
▲ − └────────────┘ │ ─────── │ │ ──────── │ ▲ −
P_ref ───────┘ + droop gain │ 1 + sT_1 │ │ 1 + sT_3 │ │
└───────────┘ └──────────┘ │
valve, a-w reheat │
V_MIN..V_MAX │
D_t·(ω−ω_ref)
turbine damping
w_d = ω − ω_ref
p_d = (P_ref − w_d) / R
dx_1/dt = (p_d − x_1) / T_1 anti-windup V_MIN..V_MAX
dx_2/dt = (x_1 − x_2) / T_3
y = (T_2/T_3)·(x_1 − x_2) + x_2
P_m = y − D_t·w_d
Parameters: R, T_1, T_2, T_3, D_t, V_MAX, V_MIN.
G2ELin’s current governor is exactly the x_1 stage, with R = m_p,
T_1 = T_G, T_2 = T_3 (lead-lag transparent) and D_t = 0. So TGOV1 is one
extra state, one extra parameter and a limit — the cheapest real model on this
list and the one I would do first.
T_2/T_3 represents the reheater: the fraction of power in the HP stage
appears fast, the rest arrives with the reheat time constant T_3 (5–10 s).
It is why steam units cannot arrest a frequency excursion as fast as their
valve moves, and it is invisible in the current model.
4.7 IEEEG1 — detailed steam governor · 6 states
Tandem-compound turbine with four steam volumes and up to eight power take-off fractions, optionally driving two generators (cross-compound).
ω_ref−ω ─►[ K(1+sT_2)/(1+sT_1) ]─►(Σ)─►[ 1/T_3 ]─►[U_C..U_O]─►[ ∫ ]─►[P_MIN..P_MAX]
▲ − (valve position)
P_ref, P_aux ──┘ ▲ │
└──────── position feedback ───┘
│
┌──────────────────────────────────────────────────────────────┘
▼
[1/(1+sT_4)]──┬──[1/(1+sT_5)]──┬──[1/(1+sT_6)]──┬──[1/(1+sT_7)]──┐
x_4 │ x_5 │ x_6 │ x_7 │
│ │ │ │
P_HP = K_1·x_4 + K_3·x_5 + K_5·x_6 + K_7·x_7 → shaft 1
P_LP = K_2·x_4 + K_4·x_5 + K_6·x_6 + K_8·x_7 → shaft 2 (cross-compound)
w_d = ω_ref − ω
dx_LL/dt = (w_d − x_LL)/T_1 ; y_LL = K·[(T_2/T_1)(w_d − x_LL) + x_LL]
v_s = (y_LL + P_ref + P_aux − x_I)/T_3 valve rate, limited U_C..U_O
dx_I/dt = v_s anti-windup P_MIN..P_MAX
dx_4/dt = (x_I − x_4)/T_4 steam chest
dx_5/dt = (x_4 − x_5)/T_5 reheater
dx_6/dt = (x_5 − x_6)/T_6 crossover
dx_7/dt = (x_6 − x_7)/T_7 second reheat
P_m = K_1·x_4 + K_3·x_5 + K_5·x_6 + K_7·x_7
The K_1..K_8 must sum to 1 (ANDES normalises them if they do not). U_C
and U_O are rate limits on the valve — closing and opening velocity —
which is a different kind of limit from all the others here and the one most
likely to bind during a real frequency event.
4.8 HYGOV — hydro governor with penstock · 4 states
The only genuinely nonlinear model on the list, and the reason it belongs here: a hydro unit that is asked for more power first delivers less, because opening the gate drops the head before the flow builds. A linear governor cannot produce that, and it changes the sign of the early frequency response.
ω−ω_ref ─►(Σ)─►[ 1/(1+sT_f) ]─►[ ∫ ]─►[G_MIN..G_MAX, ±VELM]─►[ 1/(1+sT_g) ]─► g
▲ − │
P_ref ─────┘ + ┌── transient droop r·T_r ─────────────────┤
▲ − │ │
└────────── R·(gate) ◄─┘ │
▼
h = (q/g)² ◄──── [ q: dq/dt = (1 − h)/T_w ] ────┤
water column │
P_m = A_t·h·(q − q_NL) − D_t·(ω−ω_ref)·g ◄───────┘
p_d = P_ref − (ω − ω_ref) − R·g_desired
dx_f/dt = (p_d − x_f)/T_f
dc/dt = x_f gate position integrator,
G_MIN..G_MAX, rate ±VELM
g_desired= c + x_f/r
dg/dt = (g_desired − g)/T_g gate servo
dq/dt = (1 − (q/g)²)/T_w water column ← the nonlinearity
P_m = A_t·(q/g)²·(q − q_NL) − D_t·(ω−ω_ref)·g
Parameters: R, r, T_r, T_f, T_g, V_ELM, G_MIN, G_MAX, T_w, A_t, D_t, q_NL.
T_w (water starting time, 0.5–4 s) sets the right-half-plane zero. r/T_r
is the transient droop — a temporary droop much larger than the permanent
one, which is the classical fix for exactly this non-minimum-phase behaviour.
(q/g)² is differentiable for g > 0, so this linearises fine; only the gate
limits need the §5.1 treatment.
4.9 GAST — gas turbine · 3 states
ω−ω_ref ─►(Σ)─►[LV gate]─►[ 1/(1+sT_1) ]─►[ 1/(1+sT_2) ]──┬──► P_m (−D_t·w_d)
▲ − ▲ valve, a-w turbine │
P_ref ─────┘ + │ V_MIN..V_MAX │
│ ▼
└──[ A_T + K_T(A_T − x_3) ]◄──[ 1/(1+sT_3) ]
exhaust temperature limit thermocouple
w_d = ω − ω_ref
p_d = (P_ref − w_d)/R
v_9 = A_T + K_T·(A_T − x_3) temperature-limit demand
dx_1/dt = (min(p_d, v_9) − x_1)/T_1 anti-windup V_MIN..V_MAX
dx_2/dt = (x_1 − x_2)/T_2
dx_3/dt = (x_2 − x_3)/T_3
P_m = x_2 − D_t·w_d
The low-value gate min(p_d, v_9) is the exhaust-temperature limiter, and
it is what makes a gas turbine’s frequency response asymmetric: it can shed
load freely but cannot pick it up past the temperature ceiling. It is also a
min(), i.e. non-differentiable — see §5.1.
5. Decisions to make before implementing
5.1 Limits, gates and dead-bands — the central question
Every model above has them; G2ELin has none. They are not differentiable, and G2ELin’s whole pipeline is a symbolic Jacobian.
The standard resolution, which I recommend:
For linearisation: ignore them, but check them. Small-signal analysis is about behaviour around an operating point, and at a valid operating point no limit should be binding. The right move is not to silently drop them but to evaluate every limit at the operating point and emit a warning when one is active — a machine sitting on its
V_RMAXhas no AVR loop at all, and its eigenvalues are meaningless if the limit is ignored. This fits the existingnotesmechanism (modal/adequacy.pyalready does this kind of reporting).For time-domain: implement them properly, with anti-windup on every integrator behind a limit. The nonlinear path (
nonlinear_funcs) does not need differentiability, andtimedomain/emt.pyalready uses a Newton solve that can carry a saturated branch. Note that hard limits will hurt the variable-step solvers (Radauet al.) unless they are smoothed or handled as events — this needs a decision of its own.Dead-bands and low-value gates (
GAST,IEEEG1,HYGOV) are the same problem in a sharper form.min/maxcan be linearised by picking the active branch at the operating point, which is exactly the “ignore but check” rule above.
This decision should be made before any model is written, because it determines whether a limit is a parameter carried through the symbolic model or a wrapper around it.
5.2 The reduction catalogue becomes model-dependent
reduction.py currently hard-codes the state groups:
StateGroup("pss", "Power system stabiliser", ("Dw1", "v1", "v2", "vpss"), ...)
StateGroup("avr", "AVR / exciter", ("e1", "e2", "efd", "e3"), ...)
StateGroup("governor", "Governor", ("Pm",), ("P_m",), ...)
With selectable controllers, the symbols in these groups depend on which model
the unit has. ElementModel needs to become a function of the controller
choice, or the groups need to be contributed by the controller block itself.
This is the identical problem to the deferred GFM controller work (dVOC,
VSM and Matching add and remove states relative to Droop, and VSM/Matching
drop pm/qm entirely). The two should be designed together and the
refactor done once. I would treat that as a prerequisite rather than a
parallel task.
5.3 A block architecture
sm_dae() currently builds all nineteen equations inline. I propose factoring
each controller into a function returning a small bundle:
@dataclass(frozen=True)
class ControlBlock:
states: list[sp.Symbol]
diffeqs: list[sp.Expr]
state_names: list[str]
groups: tuple[StateGroup, ...] # its own contribution to the catalogue
output: sp.Expr # V_s, or E_fd, or P_m
params: dict[str, float] # its defaults
def exciter_block(kind: ExciterModel, *, e_t, v_ref, v_s) -> ControlBlock: ...
def pss_block(kind: PssModel, *, dw_r, p_e, v_t) -> ControlBlock: ...
def governor_block(kind: GovernorModel, *, omega, w_ref, p_ref) -> ControlBlock: ...
sm_dae(is_slack, modes, exciter, pss, governor) then concatenates them. The
lru_cache key extends naturally. The existing lead-lag idiom (§2.2) gives a
shared helper:
def lead_lag(u, du, T_n, T_d, state): # returns dstate/dt
return (T_n * du + u - state) / T_d
Parameter naming needs a decision too: sm_params() is currently one flat
dict, and EXST1.T_A would collide with nothing today but TGOV1.T_1 and
IEEEG1.T_1 collide with each other. Either prefix by block (exc_TA,
gov_T1) or nest the dict. Prefixing is less disruptive — DerUnit.params
is flat, overridable_param_keys() assumes flat, and the parameter-sensitivity
scan (modal/parameters.py) reports flat names that users read.
5.4 Schema, API and UI
DerUnit.controller: GfmController | None is the precedent. Add:
exciter: ExciterModel = ExciterModel.G2ELIN_LEGACY
pss: PssModel = PssModel.G2ELIN_LEGACY
governor: GovernorModel = GovernorModel.G2ELIN_LEGACY
Keeping the present models as explicitly-named legacy members (rather than
silently remapping them onto EXDC2/PSS1A/TGOV1) is what preserves every
saved network and every golden test — including the MATLAB parity suite, which
must keep reproducing symSG.m exactly. New networks can default to the
standard models; old ones keep what they had.
The UI work is the model-order panel (web/js/model_order.js) gaining three
selectors per unit, and the parameter editor becoming model-dependent.
5.5 Validation
Each model gets a test against ANDES’s implementation of the same model, in
the shape tools/compare_andes.py already establishes: build the same
single-machine system in both, compare eigenvalues and a step response.
This is the strongest argument for the shortlist in §1 — nine of the twelve
models are checkable this way, on a package already in the environment.
Where ANDES has no counterpart (PSS2B), the alternative is a transfer-function
check: excite the block alone and compare its frequency response against the
analytic H(s) from the standard. Weaker, but not nothing.
6. Suggested phasing
Phase 0 — prerequisites. Decide §5.1 (limits). Do the §5.2 refactor,
together with the deferred GFM controller types. Add the ControlBlock
plumbing of §5.3 and the schema fields of §5.4, with the present models
wrapped unchanged as legacy. No new physics — the whole test suite must
still pass unchanged.
Phase 1 — the three that are supersets. TGOV1, PSS1A, EXDC2. Each
is a small extension of something that already works, so they exercise the new
architecture without much new risk. EXDC2 carries the K_e correction from
§2.3 and the first saturation function.
Phase 2 — the ones that add capability. EXST1 (with the Kundur figure as
a named preset, and its parameter-name trap documented), SEXS, IEEEST,
HYGOV, GAST, IEEEG1. HYGOV is the one that will exercise the nonlinear
path hardest.
Phase 3 — later, on request. PSS2B (no ANDES partner; the
integral-of-accelerating-power construction and its ramp-tracking filter
(1+sT_8)^N/(1+sT_9)^M need transcribing from IEEE 421.5 Fig. 8 directly,
and the exact block interconnection is easy to get subtly wrong — it should be
done against a paper copy of the standard, not from memory). GGOV1,
ESST4B, AC8B, PSS4B.
Separately, and not in this document’s scope: machine saturation and the
operational-parameter (Xd, X'd, T'do, …) ingestion path from §3.2. Those
are what would let G2ELin read a PSS/E .dyr file, which is a bigger prize
than any single controller model and should be planned on its own.
7. Open questions for you
Limits in the linear model — is “ignore but warn when binding” the behaviour you want, or would you rather refuse to linearise a machine sitting on a limit?
Parameter namespacing — flat with block prefixes (
exc_TA,gov_T1), or nested per block? The flat choice keepsmodal/parameters.pyand the override mechanism untouched.Defaults for new networks — keep
legacyeverywhere so nothing moves, or default new units toEXST1/PSS1A/TGOV1?Is the shortlist right for your users? It is biased toward thermal and hydro plant, which is what a transmission planner wants. Given G2ELin’s focus on converter-dominated systems, an argument could be made for spending phase 2 on the deferred GFM controllers instead, and treating synchronous-machine controls as a phase-1-only concern.
The figure’s summing-junction signs (§4.1) — can you confirm against the book?
8. What the first two models settled
Kundur Fig. E12.9’s exciter and stabiliser are in, selectable per machine via
DerUnit.exciter and DerUnit.pss. Both default to the original models, so
every saved network and every golden test linearises to exactly what it did
before; both Kundur presets carry the new ones.
8.0 Fitted, not just tuned
A machine’s governor and stabiliser are optional equipment: pss: "none"
and governor: "none" remove the states, the parameters and the reduction
group, rather than turning a gain down. That matters in three ways the
tuned-to-zero version could not manage.
It is the honest model — a machine with no governor has no droop and no
valve state, and its mechanical power is pinned to the operating point
through the same x_0 symbol a frozen state uses, so it needs no new
initialisation. It removes a preset hack: kundur_two_area_classic used to
fake “no governor” with mp = 1e6, a droop so large it would not respond,
because there was no off switch. And it makes the book’s cases directly
expressible, since Examples 12.6 and 12.9 are both worked under constant
mechanical torque.
There is an equivalence worth knowing: governor: "none" and the reduction
setting governor: frozen produce the identical A matrix, and a test pins
that. They are the same physics reached two ways — one says the machine has
no governor, the other says its governor is too slow to matter over the
window of interest. Keeping both is deliberate: the first is equipment, the
second is an approximation, and a user reaching for one is not asking the
same question as a user reaching for the other.
8.1 How the architecture came out
Lighter than §5.3 proposed. Each regulator is a _ControlBlock — its states,
their derivatives, its output, and anything it solves algebraically — and
sm_dae() splices two of them onto the fixed machine states. The blocks come
after the machine’s own states, so the default pair reproduces the original
19-state vector exactly, names and order both.
Three things fell out that the plan did not anticipate:
A thyristor exciter makes the field voltage algebraic. With no exciter
lag, E_fd follows the regulator instantaneously, so the lead-lag has direct
feedthrough and E_fd is not a state. _ControlBlock.alg exists for that.
It is also why the Kundur AVR group allows only dynamic and frozen, and
why freezing it does not mean constant field voltage — the gain path still
follows V_ref and the stabiliser.
Each model owns its parameters, under its own names. §5.3 weighed
prefixing against nesting; the answer was neither. A machine’s parameter set
is built from the models it carries — SM_EXCITER_PARAMS[exciter] plus
SM_PSS_PARAMS[pss] on top of the machine’s own — so the Kundur exciter
brings TR, KA, TA, TB and the Kundur stabiliser brings KSTAB,
TW, T1..T4, exactly as Fig. E12.9 labels them. Nothing is shared with
the original regulators and nothing is renamed to a common spelling.
That dissolves the naming trap of §4.1 rather than documenting it: TA is
the figure’s TGR lead because it is the Kundur exciter’s TA, and the
original regulator’s Ta is a different parameter of a different model that
can never be on the same machine. An override under a name the chosen model
does not have is rejected by validation instead of being silently ignored,
which a shared set could not have done.
The cost is that a unit’s key set is no longer a function of its type, so
everything that asks what a unit may override has to be told which models it
carries: overridable_param_keys(kind, exciter=, pss=), with unit_keys(der)
as the convenience for callers holding a unit. Validation, the parameter
sweep, the sensitivity scan and the defaults endpoint all go through it.
One thing to settle before governors become selectable. The Kundur stabiliser’s
T1..T4are flat keys, andTGOV1has aT1,T2,T3of its own. Today that is safe — a machine has one stabiliser and one governor, and the governor is not yet selectable — but the moment it is, those collide. §5.3’s prefixing or nesting has to be decided then, and it will be a migration of saved networks.
The state groups became per unit, not per type. §5.2 predicted this.
reduction.sm_element(exciter, pss, governor) builds the catalogue for one
machine’s set, and Network.unit_element(der) is what every per-unit caller
now goes through. A model fitted as none contributes no group at all rather
than an empty one, so the UI never draws a control with nothing under it.
And it carried the grid-forming work with it, as §5.2 hoped. All five of
symGFM_types.m’s power-control laws are now selectable per converter –
droop, droop behind a filter, dVOC, VSM and matching control – on the same
machinery: gfm_element(controller) for the groups, per-law parameter sets
under each law’s own names, and the law read off the operating point by both
the linear and the nonlinear builder. VSM and matching drop the power
filters entirely, which was the case that made a per-type catalogue
untenable in the first place.
Two things work differently there. A law brings a set of groups where a
regulator brings one, so the catalogue hands the UI the whole set to swap
in. And the parameters are written in terms of the droop tuning, as
script_generic.m writes them: eta = mp, J = 1/(mp*wf), K_theta = mp*Kpdc 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.
That second point turned out to be worth more than a comment. Because every
law is written in the same three quantities, every law can be read back as
them, which makes the tuning a thing in its own right rather than a property
of whichever law happens to be running. So mp, nq, wf and the
equivalent inertia H can be set on a VSM or a matching converter that has
no parameter by those names, and swapping a law carries the tuning across
instead of dropping the unit back to the defaults. The machine’s regulators
have no such correspondence – a rate-feedback gain is not a transient gain
reduction – so theirs are simply discarded on a swap, and that asymmetry is
the physics, not an omission.
One trap found there and worth stating generally: a state name must mean
one group. The filtered droop’s filter state and the VSM’s swing state are
both Dw, displayed dw, and they were filed under different group ids –
so looking a state up by name answered with whichever was registered last,
silently. They share one id now, and the lookup refuses to be built if any
two laws disagree about a name again. The group ids are the same whichever model is chosen, so a saved
level or per-group override survives swapping an exciter — only the symbols
behind the group change. The GFM controller work can reuse all of it.
8.2 Two things the models showed that the plan had wrong
Transient gain reduction is not a reliable fix, and the plan implied it
was. §4.1 said TGR “stops the AVR from destabilising the electromechanical
mode”. On the two-area system that is false as a general statement: damping
is not monotonic in the regulator’s transient gain. Measured on
kundur_two_area_classic, with band gain K_A·T_C/T_B:
Band gain |
200 |
100 |
40 |
20 |
10 |
2 |
|---|---|---|---|---|---|---|
Inter-area damping |
−0.7% |
−2.5% |
−4.2% |
−2.9% |
+0.0% |
+3.9% |
Detuning from full gain first makes the mode worse, bottoming out around a
fiftieth-scale gain, and only helps once the regulator is barely acting.
Which side of that trough a machine sits on depends on its loading — Kundur
Ch. 12’s K5 changing sign. Exciter detuning is therefore not the fix, which
is the actual reason the figure carries a stabiliser too.
The stabiliser acts through the exciter. It has no actuator of its own — it adds a signal at the regulator’s summing junction, and whatever the regulator does not pass never reaches the field. Its contribution falls away with the exciter’s band gain, to nothing:
Band gain |
200 |
100 |
40 |
20 |
10 |
2 |
|---|---|---|---|---|---|---|
Damping added by the PSS |
+1.8 |
+1.7 |
+1.2 |
+0.6 |
+0.0 |
−0.4 |
So TGR and a PSS are not two interchangeable fixes to pick between; detuning the exciter disarms the stabiliser. Both tables are pinned by tests.