Source code for pydynamicestimator.tests.test_recur_est_omega_modes

# Created: 2026-05-22
# Regression test for the estimator-side reference-frame fix.
#
# Before the refactor that introduced self.device_list / self.bus_init on
# Dae and the self.grid sizing in Dae.setup, calling DaeEst.estimate() with
# omega_mode in {'coi', 'single', 'dist'} would either crash inside
# casadi.substitute() (W_sym/W_expr dimension mismatch on grids of different
# sizes than grid_sim) or silently produce wrong omega expressions because
# update_omega() iterated device_list_sim regardless of whether the calling
# Dae was sim or est.
#
# This test runs run() end-to-end for each of the four omega_mode values
# with skip_est=False and asserts the estimator completes without raising
# and produces a finite, non-trivial state trajectory whose order of
# magnitude is reasonable (max|x| < 100 — a loose bound that catches
# divergence). RMSE-style truth comparison against the simulator is hard
# here because est and sim use different grids/devices in the IEEE39 test
# case (BusUnknown declarations make the est nx smaller than sim nx) — the
# sanity check is that the filter converges at all under each mode.
#
# The fifth subtest exercises the NotImplementedError guard that fires
# when line_dyn=True is set on a DaeEst instance.

from pathlib import Path

import pytest
import numpy as np

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

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


[docs] def _base_config(omega_mode): return config.updated( testsystemfile="IEEE39_bus", system_root=FIXTURE_ROOT, omega_mode=omega_mode, skip_est=False, skip_disturance=False, fn=50, Sb=100, ts=0.005, te=0.02, T_start=0.0, T_end=2.0, int_scheme="backward", int_scheme_sim="idas", init_error_diff=1, init_error_alg=True, plot=False, plot_voltage=False, plot_diff=False, proc_noise_alg=1e-3, proc_noise_diff=1e-4, filter="iekf", log_level="ERROR", incl_lim=False, line_dyn=False, )
[docs] @pytest.mark.parametrize("mode", ["nom", "coi", "single", "dist"]) def test_estimator_runs_for_each_omega_mode(mode): cfg = _base_config(mode) est, sim = run(cfg) # Estimator must have produced a populated state trajectory. assert est.x_full is not None assert est.x_full.shape[0] > 0 assert est.x_full.shape[1] > 1 # No NaN/Inf entries. x = np.asarray(est.x_full, dtype=float) assert np.all(np.isfinite(x)), f"Estimator x_full has non-finite entries in {mode} mode" # Trajectory must not have diverged catastrophically. The IEEE39 case # has bounded states (angles ~rad, speeds ~pu, exciter states ~few pu). assert np.max(np.abs(x)) < 100, ( f"Estimator diverged in {mode} mode: max|x|={np.max(np.abs(x)):.2f}" ) # dist mode adds per-bus delta_ref states — nx should be larger than # the other modes. (Confirms the delta_ref pipeline is wired.) if mode == "dist": # Other modes give nx=36 on this test case; dist adds nbus extras. assert est.nx > 36, ( f"dist mode should allocate per-bus delta_ref states; got nx={est.nx}" )
[docs] def test_estimator_rejects_line_dyn_true(): """The line_dyn guard must fire if a DaeEst ever sees line_dyn=True.""" # Run a baseline estimation to populate dae_est, then flip the flag and # call estimate() again. Mimics what would happen if someone tried to # set line_dyn=True at runtime. cfg = _base_config("nom") est, _ = run(cfg) est.line_dyn = True with pytest.raises(NotImplementedError) as excinfo: est.estimate(system.disturbance_est) assert "line_dyn=True" in str(excinfo.value) assert "PMU" in str(excinfo.value)