from __future__ import annotations
from typing import TYPE_CHECKING, Dict, List
from abc import ABC, abstractmethod
if TYPE_CHECKING:
from pydynamicestimator.system import Dae
[docs]
class AVR(ABC):
"""Abstract base class for Automatic Voltage Regulator models.
Every AVR must expose 'Efd' -- the field-voltage coupling variable consumed
by the electromagnetic equations of the synchronous machine. 'Efd' may be
declared either as a differential ``state`` (when the exciter is a pure lag,
e.g. IEEEDC1A) or as a device-private ``algeb`` (when the exciter has a
direct-feedthrough block such as a lead-lag, so its output is algebraic; see
AVRKundur). The host resolves 'Efd' wherever it lives via
``Synchronous.var_sym`` -- the machine equations are agnostic to the choice.
The AVR does NOT own state arrays or DAE indices. It declares what states,
private algebraics, parameters, noise values, etc. it needs, and the host
Synchronous machine registers them on itself.
"""
[docs]
@abstractmethod
def states(self) -> List[str]:
"""Return ordered list of differential-state names."""
...
[docs]
def algebs(self) -> List[str]:
"""Return ordered list of device-private *algebraic* variable names.
Default empty: most AVRs are pure-lag exciters whose output 'Efd' is a
state. An exciter with a direct-feedthrough (lead-lag) block returns
['Efd'] here instead of listing it in :meth:`states`, and writes its
defining residual ``0 = -Efd + <expr>`` into ``dae.g`` in :meth:`fgcall`.
These ride the device-private-algebraic mechanism (``_algebs_int``).
"""
return []
[docs]
def algebs_units(self) -> Dict[str, str]:
"""Units for each private algebraic (mirrors :meth:`units`)."""
return {}
[docs]
def algebs_noise(self) -> Dict[str, float]:
"""Relative process-noise weight for each private algebraic."""
return {}
[docs]
def algebs_x0(self) -> Dict[str, float]:
"""Initial guess for each private algebraic (Newton guess in finit)."""
return {}
[docs]
@abstractmethod
def units(self) -> List[str]:
"""Return units for each state, same length as states()."""
...
[docs]
@abstractmethod
def params(self) -> Dict[str, float]:
"""Return dict of parameter names -> default values."""
...
[docs]
@abstractmethod
def states_noise(self) -> Dict[str, float]:
"""Return noise specification for each state."""
...
[docs]
@abstractmethod
def states_init_error(self) -> Dict[str, float]:
"""Return initial error for each state."""
...
[docs]
@abstractmethod
def x0(self) -> Dict[str, float]:
"""Return default initial guess for each state."""
...
[docs]
@abstractmethod
def descriptions(self) -> Dict[str, str]:
"""Return descriptions for states and params."""
...
[docs]
@abstractmethod
def setpoints(self) -> Dict[str, float]:
"""Return setpoint names -> defaults (e.g., Vf_ref)."""
...
[docs]
@abstractmethod
def fgcall(self, host, dae: Dae) -> None:
"""Write the AVR's differential equations into ``dae.f`` and, if the AVR
declares private algebraics, their defining residuals into ``dae.g``.
Args:
host: The Synchronous machine instance. Access state/algebraic
indices via host.Efd, host.Rf, etc. and parameters via
host.KA, etc.
dae: The DAE system object.
"""
...
[docs]
class IEEEDC1A(AVR):
"""IEEEDC1A exciter and AVR model as presented in Power System Dynamics
and Stability by P.W. Sauer and M.A. Pai, 2006. (page 100)
States: Efd, Rf, Vr (3 states)
"""
[docs]
def states(self) -> List[str]:
return ["Efd", "Rf", "Vr"]
[docs]
def units(self) -> List[str]:
return ["p.u.", "p.u.", "p.u."]
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TA": 0.015,
"KF": 1.0,
"TF": 0.1,
"KE": 1.0,
"TE": 0.04,
"Vr_max": 5.0,
"Vr_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Efd": 1, "Rf": 1, "Vr": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Efd": 0.1, "Rf": 0.1, "Vr": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Efd": 1.5, "Rf": 0.2, "Vr": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TA": "voltage regulator time constant",
"KF": "stabilizer gain",
"TF": "stabilizer time constant",
"KE": "exciter field constant without saturation",
"TE": "exciter time constant",
"Efd": "internal field voltage",
"Rf": "feedback rate",
"Vr": "pilot exciter voltage",
"Vf_ref": "exciter set point voltage",
"Vr_min": "Exciter minimal voltage",
"Vr_max": "Exciter maximal voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
dae.f[host.Efd] = 1 / host.TE * (-(host.KE) * dae.x[host.Efd] + dae.x[host.Vr])
dae.f[host.Rf] = (
1 / host.TF * (-dae.x[host.Rf] + host.KF / host.TF * dae.x[host.Efd])
)
dae.f[host.Vr] = (
dae.s[host.Vr]
* 1
/ host.TA
* (
-dae.x[host.Vr]
+ host.KA * dae.x[host.Rf]
- host.KA * host.KF / host.TF * dae.x[host.Efd]
+ host.KA
* (
host.Vf_ref
- sqrt((dae.y[host.vre]) ** 2 + (dae.y[host.vim]) ** 2)
+ host.pss_signal(dae)
)
)
)
[docs]
class AVRKundur_Filter(AVR):
"""AVR model used in Kundur's book (Power System Stability and Control, 1994)
for the 2-area system. A filter is added to the AVR output to prevent unrealistic fast dynamics and improve numerical stability.
"""
[docs]
def states(self) -> List[str]:
return ["Efd", "Vl", "Vtr"]
[docs]
def units(self) -> List[str]:
return ["p.u.", "p.u.", "p.u."]
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TA": 1,
"TB": 10,
"TR": 0.01,
"Tfd": 0.01,
"Efd_max": 5.0,
"Efd_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Efd": 1, "Vl": 1, "Vtr": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Efd": 0.1, "Vl": 0.1, "Vtr": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Efd": 1.5, "Vl": 1.5, "Vtr": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TA": "AVR lead time constant",
"TB": "AVR lag time constant",
"TR": "Transducer time constant",
"Tfd": "exciter field filter time constant",
"Efd_max": "Maximum field voltage",
"Efd_min": "Minimum field voltage",
"Efd": "internal field voltage",
"Vl": "Lead lag voltage state",
"Vtr": "Transducer voltage state",
"Vf_ref": "exciter set point voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
# Lead-lag block
dae.f[host.Vtr] = (
1
/ host.TR
* (-dae.x[host.Vtr] + sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2))
)
dae.f[host.Vl] = (
1
/ host.TB
* (
-dae.x[host.Vl]
+ host.KA
* (host.Vf_ref - dae.x[host.Vtr] + host.pss_signal(dae))
)
)
dae.f[host.Efd] = (
1
/ host.Tfd
* (
-dae.x[host.Efd]
+ dae.x[host.Vl]
+ host.TA
/ host.TB
* (
host.KA
* (-dae.x[host.Vtr] + host.Vf_ref + host.pss_signal(dae))
)
- host.TA / host.TB * (dae.x[host.Vl])
)
)
[docs]
class AVRKundur(AVR):
r"""Kundur 2-area AVR as a transducer + lead-lag, with the field voltage
'Efd' declared as a device-private ALGEBRAIC variable.
This is :class:`AVRKundur_Filter` with the parasitic output filter removed.
The filter (pole ``1/(1 + s*Tfd)``) existed only to fake the lead-lag output
into a differential state; physically the lead-lag
Efd = KA * (1 + s*TA) / (1 + s*TB) * (Vf_ref - Vtr)
is proper-but-not-strictly-proper, so its output has a *direct feedthrough*
and is genuinely algebraic. The faithful realization is one lag pole state
``Vl`` plus the algebraic output:
Vtr_dot = (1/TR) (-Vtr + |V|) # transducer
Vl_dot = (1/TB) (-Vl + KA (Vf_ref - Vtr)) # lag pole state
0 = -Efd + Vl (1 - TA/TB) + (TA/TB) KA (Vf_ref - Vtr) # Efd algebraic
The third line is the lead feedthrough ``D = TA/TB``; one verifies
``Vl(1-TA/TB) + (TA/TB)KA(Vf_ref-Vtr) = KA(1+sTA)/(1+sTB)(Vf_ref-Vtr)``.
'Efd' is therefore exposed via :meth:`algebs` (not :meth:`states`) and rides
the device-private-algebraic mechanism; the host reads it through
``Synchronous.var_sym('Efd')``. As ``Tfd -> 0`` the filtered model converges
to this one (singular-perturbation limit) -- the controller-side counterpart
of the SP6-DAE machine demonstration. See docs/algebraic_equations_design.md.
"""
[docs]
def states(self) -> List[str]:
return ["Vl", "Vtr"] # lag pole state + transducer state (no Efd)
[docs]
def units(self) -> List[str]:
return ["p.u.", "p.u."]
[docs]
def algebs(self) -> List[str]:
return ["Efd"] # field voltage = lead-lag algebraic output (feedthrough)
[docs]
def algebs_units(self) -> Dict[str, str]:
return {"Efd": "p.u."}
[docs]
def algebs_noise(self) -> Dict[str, float]:
return {"Efd": 1.0}
[docs]
def algebs_x0(self) -> Dict[str, float]:
return {"Efd": 1.5}
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TA": 1,
"TB": 10,
"TR": 0.01,
"Efd_max": 5.0,
"Efd_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Vl": 1, "Vtr": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Vl": 0.1, "Vtr": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Vl": 1.5, "Vtr": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TA": "AVR lead time constant",
"TB": "AVR lag time constant",
"TR": "Transducer time constant",
"Efd_max": "Maximum field voltage",
"Efd_min": "Minimum field voltage",
"Efd": "internal field voltage (algebraic lead-lag output)",
"Vl": "lag pole state",
"Vtr": "Transducer voltage state",
"Vf_ref": "exciter set point voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
Vt = sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2)
# Transducer lag (differential)
dae.f[host.Vtr] = 1 / host.TR * (-dae.x[host.Vtr] + Vt)
# Lag pole state of the lead-lag (differential)
dae.f[host.Vl] = (
1 / host.TB * (-dae.x[host.Vl] + host.KA * (host.Vf_ref - dae.x[host.Vtr] + host.pss_signal(dae)))
)
# Field voltage: algebraic lead-lag output (lead feedthrough TA/TB).
# Residual convention 0 = -Efd + <expr>, matching the device-private
# mechanism (see SynchronousSubtransientSP_DAE).
dae.g[host.Efd] = (
-dae.y[host.Efd]
+ dae.x[host.Vl] * (1 - host.TA / host.TB)
+ (host.TA / host.TB) * host.KA * (host.Vf_ref - dae.x[host.Vtr] + host.pss_signal(dae))
)
[docs]
class AVRKundur_NoTGR(AVR):
r"""AVRKundur with the transient gain reduction (the lead-lag
``(1+sTA)/(1+sTB)``) removed: a plain high-gain static exciter with only a
terminal-voltage transducer.
Efd = KA * (Vf_ref - Vtr), Vtr = Vt / (1 + s*TR)
States: ``Vtr`` (transducer). ``Efd`` is the algebraic output
``KA*(Vf_ref - Vtr)`` -- an instantaneous gain on the transduced error, so it
is declared as a private algebraic (read by the machine via
``Synchronous.var_sym('Efd')``). Parameters: ``KA``, ``TR``.
With a high ``KA`` and no TGR this exciter typically *reduces* the damping of
the electromechanical modes (it can push them toward / into the RHP), which is
the classic setting in which a power system stabilizer (PSS) is needed to
restore damping. The PSS signal enters at the summing junction via
``host.pss_signal(dae)`` (0 when no PSS is attached).
"""
[docs]
def states(self) -> List[str]:
return ["Vtr"] # terminal-voltage transducer state
[docs]
def units(self) -> List[str]:
return ["p.u."]
[docs]
def algebs(self) -> List[str]:
return ["Efd"] # field voltage = static gain on the transduced error
[docs]
def algebs_units(self) -> Dict[str, str]:
return {"Efd": "p.u."}
[docs]
def algebs_noise(self) -> Dict[str, float]:
return {"Efd": 1.0}
[docs]
def algebs_x0(self) -> Dict[str, float]:
return {"Efd": 1.5}
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TR": 0.02,
"Efd_max": 5.0,
"Efd_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Vtr": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Vtr": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Vtr": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TR": "Transducer time constant",
"Efd_max": "Maximum field voltage",
"Efd_min": "Minimum field voltage",
"Efd": "internal field voltage (algebraic static-gain output)",
"Vtr": "Transducer voltage state",
"Vf_ref": "exciter set point voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
Vt = sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2)
# Transducer lag (differential)
dae.f[host.Vtr] = 1 / host.TR * (-dae.x[host.Vtr] + Vt)
# Field voltage: static gain on the transduced error (algebraic).
dae.g[host.Efd] = -dae.y[host.Efd] + host.KA * (
host.Vf_ref - dae.x[host.Vtr] + host.pss_signal(dae)
)
[docs]
class AVRKundur_ODE(AVR):
r"""AVR model used in Kundur's book (Power System Stability and Control, 1994)
for the 2-area system example with transient gain reduction.
This is the all-ODE realization of the same transducer + lead-lag controller
as :class:`AVRKundur`: it carries the same loop transfer function
``KA (1+sTA)/(1+sTB)`` on the error ``e = Vf_ref - Vtr`` and the same DC gain
``KA``, but realizes the lead as a derivative of the *measurement* (states
``Efd, Vtr``; ``Efd`` is a differential state via the ``TB`` lag, so no
parasitic filter is needed). The only difference from :class:`AVRKundur` is the
omitted setpoint-derivative term ``KA*TA*dVf_ref/dt`` -- identically zero for a
constant ``Vf_ref`` (always, here), so the two produce the same ``Efd``.
:class:`AVRKundur` instead keeps ``Efd`` as an algebraic variable (the lead's
direct feedthrough); :class:`AVRKundur_Filter` is :class:`AVRKundur` plus a
parasitic output pole ``1/(1+sTfd)`` that turns ``Efd`` back into a state.
"""
[docs]
def states(self) -> List[str]:
return ["Efd", "Vtr"]
[docs]
def units(self) -> List[str]:
return ["p.u.", "p.u."]
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TA": 0.015,
"TB": 0.02,
"TR": 0.01,
"Efd_max": 5.0,
"Efd_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Efd": 1, "Vtr": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Efd": 0.1, "Vtr": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Efd": 1.5, "Vtr": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TA": "AVR lead time constant",
"TB": "AVR lag time constant",
"TR": "Transducer time constant",
"Efd_max": "Maximum field voltage",
"Efd_min": "Minimum field voltage",
"Efd": "internal field voltage",
"Vl": "Lead lag voltage state",
"Vtr": "Transducer voltage state",
"Vf_ref": "exciter set point voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
# Lead-lag block
dae.f[host.Vtr] = (
1
/ host.TR
* (-dae.x[host.Vtr] + sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2))
)
dae.f[host.Efd] = (
1
/ host.TB
* (
-dae.x[host.Efd]
+ (
host.KA
* (-dae.x[host.Vtr] + host.Vf_ref + host.pss_signal(dae))
)
- (host.TA * host.KA)
/ host.TR
* (-dae.x[host.Vtr] + sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2))
)
)
[docs]
class SEXST(AVR):
"""AVR model used in Kundur's book (Power System Stability and Control, 1994)
for the 2-area system example with transient gain reduction.
"""
[docs]
def states(self) -> List[str]:
return ["Efd"]
[docs]
def units(self) -> List[str]:
return ["p.u."]
[docs]
def params(self) -> Dict[str, float]:
return {
"KA": 200.0,
"TE": 0.1,
"Efd_max": 5.0,
"Efd_min": 0.0,
}
[docs]
def states_noise(self) -> Dict[str, float]:
return {"Efd": 1}
[docs]
def states_init_error(self) -> Dict[str, float]:
return {"Efd": 0.1}
[docs]
def x0(self) -> Dict[str, float]:
return {"Efd": 1.5}
[docs]
def descriptions(self) -> Dict[str, str]:
return {
"KA": "voltage regulator gain",
"TE": "Exciter time constant",
"Efd_max": "Maximum field voltage",
"Efd_min": "Minimum field voltage",
"Efd": "internal field voltage",
"Vf_ref": "exciter set point voltage",
}
[docs]
def setpoints(self) -> Dict[str, float]:
return {"Vf_ref": 2.0}
[docs]
def fgcall(self, host, dae: Dae) -> None:
from pydynamicestimator.devices.device import sqrt
dae.f[host.Efd] = (
1
/ host.TE
* (
-dae.x[host.Efd]
+ host.KA
* (
host.Vf_ref
- sqrt(dae.y[host.vre] ** 2 + dae.y[host.vim] ** 2)
+ host.pss_signal(dae)
)
)
)
AVR_REGISTRY: Dict[str, type] = {
"IEEEDC1A": IEEEDC1A,
"AVRKundur_Filter": AVRKundur_Filter,
"AVRKundur": AVRKundur,
"AVRKundur_NoTGR": AVRKundur_NoTGR,
"AVRKundur_ODE": AVRKundur_ODE,
"SEXST": SEXST,
}