From f1f692bd7ea74956b3da8c08a1648554d17c6218 Mon Sep 17 00:00:00 2001 From: marko1olo Date: Sun, 16 Aug 2026 22:03:14 +0400 Subject: [PATCH 1/2] fix(mateqn): make _is_symmetric numerically robust and scale-aware Use relative tolerance (rtol * norm) instead of absolute atol in _is_symmetric to avoid false negatives on large-magnitude matrices. Fixes #1174 --- control/mateqn.py | 11 +++++------ control/tests/mateqn_test.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/control/mateqn.py b/control/mateqn.py index b23e42def..60744afae 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -14,7 +14,7 @@ import numpy as np import scipy as sp -from numpy import eye, finfo, inexact +from numpy import eye from scipy.linalg import eigvals, solve from .exception import ControlArgument, ControlDimension, ControlSlycot, \ @@ -69,7 +69,7 @@ def _warn_ill_conditioned_E(E): not measurably more accurate in that regime. """ condE = np.linalg.cond(E) - if condE > 1.0 / np.sqrt(finfo(float).eps): + if condE > 1.0 / np.sqrt(np.finfo(float).eps): warnings.warn( f"E is ill-conditioned (cond(E) = {condE:.2g}); the generalized " "Lyapunov solution may have reduced accuracy. The problem itself " @@ -383,7 +383,7 @@ def dlyap(A, Q, C=None, E=None, method=None): # Solvability requires lam_A * lam_Q != 1 for all pairs of # eigenvalues (the diagonals of the triangular factors) if np.min(np.abs(np.outer(np.diag(Tq), np.diag(Ta)) - 1.)) \ - < finfo(float).eps * max( + < np.finfo(float).eps * max( 1., np.abs(np.diag(Ta)).max() * np.abs(np.diag(Tq)).max()): raise ControlArgument( @@ -787,8 +787,7 @@ def _check_shape(M, n, m, square=False, symmetric=False, name="??"): # Utility function to check if a matrix is symmetric def _is_symmetric(M): M = np.atleast_2d(M) - if isinstance(M[0, 0], inexact): - eps = finfo(M.dtype).eps - return ((M - M.T) < eps).all() + if issubclass(M.dtype.type, (np.inexact, float, complex)): + return np.allclose(M, M.conj().T) else: return (M == M.T).all() diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 5cc34c2ab..6dbb89df9 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -44,6 +44,25 @@ class TestMatrixEquations: + + def test_is_symmetric_robustness(self): + """Test _is_symmetric with scale-aware tolerance and asymmetric matrices (Issue #1174).""" + from control.mateqn import _is_symmetric + # Truly symmetric + M_sym = array([[1.0, 2.0], [2.0, 1.0]]) + assert _is_symmetric(M_sym) + + # Scale-aware numerical roundoff symmetry + M_roundoff = array([[1e6, 2.0 + 1e-12], [2.0, 1e6]]) + assert _is_symmetric(M_roundoff) + + # Asymmetric with negative difference (previously failed due to missing abs()) + M_asym = array([[1.0, 0.0], [10.0, 1.0]]) + assert not _is_symmetric(M_asym) + + # Complex Hermitian matrix + M_herm = array([[1.0, 1.0 - 2.0j], [1.0 + 2.0j, 3.0]]) + assert _is_symmetric(M_herm) """These are tests for the matrix equation solvers in mateqn.py""" @pytest.mark.parametrize('method', From 8f0d749791eb20910cb05e1dabe44346643d433c Mon Sep 17 00:00:00 2001 From: marko1olo Date: Wed, 19 Aug 2026 18:52:11 +0400 Subject: [PATCH 2/2] style(tests): move class docstring before test methods per review --- control/tests/mateqn_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 6dbb89df9..f947e75f1 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -44,6 +44,7 @@ class TestMatrixEquations: + """These are tests for the matrix equation solvers in mateqn.py""" def test_is_symmetric_robustness(self): """Test _is_symmetric with scale-aware tolerance and asymmetric matrices (Issue #1174).""" @@ -63,7 +64,7 @@ def test_is_symmetric_robustness(self): # Complex Hermitian matrix M_herm = array([[1.0, 1.0 - 2.0j], [1.0 + 2.0j, 3.0]]) assert _is_symmetric(M_herm) - """These are tests for the matrix equation solvers in mateqn.py""" + @pytest.mark.parametrize('method', ['scipy',