Source code for pydynamicestimator.tests.test_post_fault_omega_modes

"""Regression tests: simulation must stay finite and bounded after a fault
for every reference-frame mode (nom, coi, single, dist) and both line_dyn
settings.

Covers the bug where DaeSim.exec_dist() rebuilt self.x via init_symbolic()
but did not refresh self.omega_ref_*_expr, leaving the post-fault integrator
substituting placeholders with expressions that referenced the pre-rebuild
SX symbols. The symptom was post-fault divergence or garbage trajectories
for every omega_mode except 'nom' (whose ω_ref is a numeric constant).
"""

from pathlib import Path

import numpy as np
import pytest

from pydynamicestimator.config import config
from pydynamicestimator.run import run

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


[docs] def _base_config(omega_mode: str, line_dyn: bool, testsystemfile: str): return config.updated( testsystemfile=testsystemfile, system_root=FIXTURE_ROOT, omega_mode=omega_mode, omega_single_idx="GFMI2", line_dyn=line_dyn, incl_lim=True, skip_est=True, plot=False, plot_voltage=False, plot_diff=False, int_scheme_sim="cvodes" if line_dyn else "idas", ts=0.0005 if line_dyn else 0.005, T_start=0.0, T_end=2.0, log_level="WARNING", )
[docs] def _omega_state_indices(sim) -> list[int]: """Return indices of states whose name ends in '_omega' (synchronous-machine rotor speeds and similar). Used as a sanity-check probe.""" return [i for i, s in enumerate(sim.states) if s.endswith("_omega")]
[docs] def _assert_post_fault_healthy(sim, fault_time: float) -> None: # Whole trajectory must be finite. assert np.all(np.isfinite(sim.x_full)), "non-finite state encountered" # Probe rotor-speed-like states post fault — these should stay near 1 p.u. # for a small disturbance over the test horizon. This catches the # "integrator silently produced garbage" failure mode. fault_step = int(round(fault_time / sim.t)) omega_idx = _omega_state_indices(sim) if omega_idx: post = sim.x_full[np.ix_(omega_idx, range(fault_step, sim.x_full.shape[1]))] assert np.all(post > 0.90) and np.all( post < 1.10 ), f"omega state left plausible band: min={post.min()}, max={post.max()}" # If dist mode, delta_ref states must stay bounded (no runaway). if sim.has_delta_ref: delta_post = sim.x_full[ np.ix_(list(sim.idx_delta_ref), range(fault_step, sim.x_full.shape[1])) ] assert np.all(np.abs(delta_post) < 1e3), "delta_ref state runaway"
[docs] @pytest.mark.parametrize("omega_mode", ["nom", "coi", "single", "dist"]) @pytest.mark.parametrize("line_dyn", [True, False]) def test_open_line_post_fault(omega_mode: str, line_dyn: bool) -> None: """3_bus_lineopen: OPEN_LINE at t=1.0s. Sim must complete and stay sane.""" cfg = _base_config(omega_mode, line_dyn, testsystemfile="3_bus_lineopen") _, sim = run(cfg) assert sim.x_full.shape[1] == sim.nts _assert_post_fault_healthy(sim, fault_time=1.0)
[docs] @pytest.mark.parametrize("omega_mode", ["nom", "coi", "single", "dist"]) @pytest.mark.parametrize("line_dyn", [True, False]) def test_bus_fault_post_clear(omega_mode: str, line_dyn: bool) -> None: """3_bus_busfault: FAULT_BUS at t=1.0s, CLEAR at t=1.04s. Sim must complete and recover to plausible state.""" cfg = _base_config(omega_mode, line_dyn, testsystemfile="3_bus_busfault") _, sim = run(cfg) assert sim.x_full.shape[1] == sim.nts # Use the clear time as the start of the recovery window — that's when # the network returns to a near-nominal topology. _assert_post_fault_healthy(sim, fault_time=1.04)