# 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)