Source code for hermess.tests.test_avr_algebraic

# © 2024-2026 ETH Zurich
# Original author: Milos Katanic
# Simulation-only fork & maintainer: Maitraya Avadhut Desai
#
# Licensed under the GNU General Public License v3.0;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     https://www.gnu.org/licenses/gpl-3.0.en.html
#
# This software is distributed "AS IS", WITHOUT WARRANTY OF ANY KIND,
# express or implied. See the License for specific language governing
# permissions and limitations under the License.
#
# Simulation-only fork of PowerDynamicEstimator
# (https://doi.org/10.5905/ethz-1007-842); dynamic state estimation removed.
# For inquiries, contact: mdesai@ethz.ch

"""Controller-side parity test for the device-private algebraic-equation mechanism.

The generalized AVR strategy protocol lets an exciter expose its field voltage
``Efd`` as *either* a differential state (pure-lag exciter) *or* a device-private
algebraic variable (direct-feedthrough / lead-lag exciter). This test exercises
the latter via ``AVRKundur`` -- the Kundur transducer+lead-lag AVR with
``Efd`` as the lead's algebraic output -- and validates it two ways:

1. The recovered ``Efd`` (in ``dae.y``) exactly satisfies its defining equation
   along the trajectory (internal consistency of the private-algebraic solve).
2. The lead-lag (algebraic ``Efd``) reproduces ``AVRKundur_Filter`` -- the same
   exciter with a parasitic output filter that fakes ``Efd`` into a state -- in
   the ``Tfd -> 0`` singular-perturbation limit. The ``avr_filter`` fixture uses
   a small ``Tfd``, so the two systems must agree to ``O(Tfd)``.
"""

from pathlib import Path

import numpy as np

from hermess.config import config
from hermess.run import run

FIXTURE_ROOT = Path(__file__).parent / "fixtures"

_COMMON = dict(
    system_root=FIXTURE_ROOT,
    fn=50,
    Sb=100,
    ts=0.005,
    T_start=0.0,
    T_end=6.0,
    int_scheme_sim="idas",
    plot=False,
    plot_voltage=False,
    plot_diff=False,
    log_level="ERROR",
    incl_lim=False,
    line_dyn=False,
    print_power_flow=False,
)


[docs] def _machine(): """The synchronous-machine device from the most recent run. ``run`` resets ``system`` (clear_module) on entry, so this must be called immediately after a run and before the next one. """ from hermess import system as _sys return next( d for d in _sys.device_list_sim if getattr(d, "_name", "").startswith("Synchronous") )
[docs] def test_avr_leadlag_constraint_holds(): """The lead-lag's algebraic field voltage must satisfy its defining equation along the whole trajectory: the recovered Efd (in y_full) equals Vl*(1 - TA/TB) + (TA/TB)*KA*(Vf_ref - Vtr) evaluated on the states.""" sim = run(config.updated(testsystemfile="avr_leadlag", **_COMMON)) m = _machine() assert sim.n_priv == 1 # Efd only x = np.array(sim.x_full) y = np.array(sim.y_full) efd = y[m.Efd[0]] # algebraic, recovered from the DAE solve Vl = x[m.Vl[0]] Vtr = x[m.Vtr[0]] TA, TB, KA, Vfr = m.TA[0], m.TB[0], m.KA[0], m.Vf_ref[0] expr = Vl * (1 - TA / TB) + (TA / TB) * KA * (Vfr - Vtr) assert np.abs(efd - expr).max() < 1e-6, ( f"Efd violates its lead-lag defining equation by " f"{np.abs(efd - expr).max():.3e} (expected < 1e-6)." )
[docs] def test_avr_leadlag_matches_filtered_limit(): """AVRKundur (Efd algebraic) must reproduce AVRKundur_Filter (Efd a state behind a fast parasitic filter) in the Tfd -> 0 limit. The avr_filter fixture uses Tfd = 1e-3, so the machine response must agree to O(Tfd).""" sim_f = run(config.updated(testsystemfile="avr_filter", **_COMMON)) mf = _machine() xf = np.array(sim_f.x_full) efd_f = xf[mf.Efd[0]] # Efd is a STATE in the filtered model delta_f = xf[mf.delta[0]] omega_f = xf[mf.omega[0]] sim_l = run(config.updated(testsystemfile="avr_leadlag", **_COMMON)) ml = _machine() xl = np.array(sim_l.x_full) yl = np.array(sim_l.y_full) efd_l = yl[ml.Efd[0]] # Efd is ALGEBRAIC in the lead-lag model delta_l = xl[ml.delta[0]] omega_l = xl[ml.omega[0]] # Structural difference: removing the filter drops one differential state # (Efd) and adds one private algebraic. assert sim_f.n_priv == 0 and sim_l.n_priv == 1 assert sim_l.nx == sim_f.nx - 1 assert sim_l.ny == sim_f.ny + 1 n = min(xf.shape[1], xl.shape[1]) assert np.isfinite(efd_l).all() # Machine dynamics (slow states) are essentially identical... assert np.abs(delta_f[:n] - delta_l[:n]).max() < 1e-3 assert np.abs(omega_f[:n] - omega_l[:n]).max() < 1e-4 # ...and the field voltage agrees to the filter-time-constant order (Tfd=1e-3). assert np.abs(efd_f[:n] - efd_l[:n]).max() < 5e-3, ( f"Algebraic Efd differs from the Tfd->0 filtered Efd by " f"{np.abs(efd_f[:n] - efd_l[:n]).max():.3e} (expected O(Tfd), < 5e-3)." )