Skip to content

Improve numerical symmetry check - #1248

Merged
slivingston merged 1 commit into
python-control:mainfrom
CNZHM666:fix-symmetry-check
Sep 29, 2026
Merged

slivingston merged 1 commit into
python-control:mainfrom
CNZHM666:fix-symmetry-check

Conversation

@CNZHM666

@CNZHM666 CNZHM666 commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

This PR addresses #1174.

The current symmetry check does not properly handle complex Hermitian matrices. It also uses a fixed floating-point tolerance. Since floating-point rounding error depends on the numerical scale of the matrix, using a fixed tolerance can be too strict for matrices with large values.

I changed the check to use the conjugate transpose (M.conj().T) and a scale-aware tolerance based on the matrix norm and floating-point spacing.

I added tests for large-scale floating-point matrices, clearly asymmetric matrices, and complex Hermitian matrices.

AI disclosure:
I used ChatGPT to help me understand the numerical formulas involved in this issue and to assist with parts of the code changes and tests. I reviewed the changes myself, ran the tests locally, and understand the submitted code.

@slivingston
slivingston self-requested a review September 15, 2026 00:21
Comment thread control/mateqn.py
Comment thread control/tests/mateqn_test.py
@coveralls

coveralls commented Sep 15, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 94.757%. remained the same — CNZHM666:fix-symmetry-check into python-control:main

@slivingston

Copy link
Copy Markdown
Member

@CNZHM666 Can you verify that your GitHub account is associated with the email address in your commit? (Until this is done, the avatar next to the commit appears generic in this PR.)

Comment thread control/tests/mateqn_test.py Outdated
from scipy.linalg import eigvals, solve

from control.mateqn import lyap, dlyap, care, dare
from control.mateqn import lyap, dlyap, care, dare, _is_symmetric

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from control.mateqn import lyap, dlyap, care, dare, _is_symmetric
from control.mateqn import lyap, dlyap, care, dare, _is_symmetric

I have not yet started a technical review... but I continue to find whitespace/style problems. Please read https://peps.python.org/pep-0008/

@ilayn

ilayn commented Sep 15, 2026

Copy link
Copy Markdown

@slivingston

Copy link
Copy Markdown
Member

@ilayn thanks for the link!

I read through issue #1174, and indeed, the agreed solution is to use the method from SciPy. @CNZHM666 Can you do so? In particular, read #1174 (comment) and #1174 (comment)

@CNZHM666

Copy link
Copy Markdown
Contributor Author

@slivingston Sure, I’ll review those comments and update the implementation accordingly.

Comment thread control/mateqn.py Outdated
Comment on lines +790 to +793
return (
sp.linalg.norm(M - M.conj().T, 1)
<= np.spacing(sp.linalg.norm(M, 1)) * 100
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the "SciPy symmetry check method" in this commit?

A few quick questions:

  1. Can you read the comments that I linked to previously, which describe the desired changes, without using an AI bot?
  2. Can you try to write the change without using an AI bot?

Using AI tools is OK, but I want to make sure that you understand the proposed solution (and thus, can write/understand the code).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I read the linked comments and SciPy documentation myself.

I misunderstood the requested change earlier. I now understand that I should use SciPy’s built-in symmetry/Hermitian check rather than reimplementing the norm/spacing test. I will rewrite the change myself.

Should the implementation use SciPy’s default exact comparison, or should atol/rtol also be passed through?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CNZHM666 I recommend that atol/rtol are passed through. Let me highlight text from one of the comments that I linked:

A couple of thoughts on things we might do:

  • We should almost certainly replace _issymmetric with scipy.issymmetric, since there is no reason for the duplication.
  • We could add a way to allow rtol and atol to be passed through to scipy.issymmetric, so that the user can control the behavior better. There are several other examples where we pass down options to scipy functions.
  • We might also include an option to symmetrize either Q or QN (via (M + M.T)*0.5, as @ilayn suggests).

Whoever picks up this issue should look through the code and see what makes the most sense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced the call chain. lqe/dlqe already accept keyword arguments and call care/dare, while care, dare, lyap, and dlyap currently do not expose symmetry tolerances. Do you want atol/rtol to be added to all of these public matrix-equation functions, or only passed through the LQE path for this issue?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to use kwargs or a similar name to support passing through the parameter to scipy. (Probably better not to add explict atol, rtol parameters.) As in the above comment, "There are several other examples where we pass down options to scipy functions." Find those examples, and try to follow the pattern in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I followed your suggestion by replacing the original symmetry check with SciPy's built-in symmetry/Hermitian checks. I also used a symmetric_kwargs pass-through so parameters such as atol and rtol can be passed from the upper-level APIs down to SciPy.I also added tests to verify that a nearly symmetric matrix fails with the default exact check but succeeds when an appropriate tolerance is passed through.

@murrayrm murrayrm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please simplify the implementation by avoiding the use of **kwargs and just using symmetric_kwargs={} in the function definitions. This will simplify the code and let you remove the kwargs_test.py changes as well.

Comment thread control/mateqn.py Outdated


def dlyap(A, Q, C=None, E=None, method=None):
def dlyap(A, Q, C=None, E=None, method=None, **kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to implement this using **kwargs rather than just using symmetric_kwargs={} in the function signature. The latter would remove the need to add kwargs tests, both here and in tests/.

Comment thread control/mateqn.py Outdated

def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
_As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E"):
_As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E", **kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I suggest implementing as symmetric_kwargs={}.

Comment thread control/mateqn.py Outdated
Comment on lines +331 to +335

symmetric_kwargs = kwargs.pop("symmetric_kwargs", {})
# Make sure there were no extraneous keywords
if kwargs:
raise TypeError("unrecognized keyword(s): ", str(kwargs))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this code could be removed by using symmetric_kwargs={} in the function definition.

Comment thread control/mateqn.py Outdated
Comment on lines +521 to +527

symmetric_kwargs = kwargs.pop("symmetric_kwargs", {})

# Make sure there were no extraneous keywords
if kwargs:
raise TypeError("unrecognized keyword(s): ", str(kwargs))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed.

Comment thread control/mateqn.py Outdated

def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
_As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E"):
_As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E", **kwargs):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, use symmetric_kwargs={}.

Comment thread control/mateqn.py Outdated
Comment on lines +687 to +693

symmetric_kwargs = kwargs.pop("symmetric_kwargs", {})

# Make sure there were no extraneous keywords
if kwargs:
raise TypeError("unrecognized keyword(s): ", str(kwargs))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

Comment thread control/mateqn.py Outdated
Comment on lines +828 to +831
symmetric_kwargs = (
symmetric_kwargs.copy() if symmetric_kwargs else {}
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this required? I think you can just pass **symmetric_kwargs to ishermetian and issymmetric.

Comment thread control/stochsys.py Outdated
Comment on lines +139 to +140
symmetric_kwargs = kwargs.pop('symmetric_kwargs', {})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK to keep since kwargs was already present, but you could also just use symmetric_kwargs={} in the function definition (before **kwargs).

Comment thread control/stochsys.py Outdated
Comment on lines +263 to +264
symmetric_kwargs = kwargs.pop('symmetric_kwargs', {})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, you could replace this with symmetric_kwargs={} in the function definition.

@CNZHM666 CNZHM666 Sep 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@murrayrm Updated as suggested: symmetric_kwargs is now an explicit parameter, and the related kwargs_test.py changes have been removed. All relevant tests and lint checks pass. Since test_mutable_defaults rejects {} as a default value, I used None and convert it to an empty dict only when needed.

@CNZHM666
CNZHM666 requested a review from murrayrm September 24, 2026 01:17
@murrayrm

Copy link
Copy Markdown
Member

@slivingston Are you OK with this version?

Comment thread control/stochsys.py Outdated
Comment thread control/stochsys.py Outdated
Comment thread control/stochsys.py Outdated
Comment thread control/stochsys.py Outdated
Comment thread control/mateqn.py Outdated
Comment thread control/mateqn.py Outdated
Comment thread control/mateqn.py Outdated
'slycot' and 'scipy'. If set to None (default), try 'slycot' first
and then 'scipy'.
symmetric_kwargs : dict, optional
Keyword arguments passed to the SciPy symmetry/Hermitian check,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread control/mateqn.py Outdated
Comment thread control/mateqn.py Outdated
Comment thread control/mateqn.py Outdated
# Solve the Lyapunov equation using SciPy
return sp.linalg.solve_continuous_lyapunov(A, -Q)
# Solve the Lyapunov equation using SciPy
return sp.linalg.solve_continuous_lyapunov(A, -Q)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this indented again? Given all of the whitespace errors in your changes, I recommend you check your editor settings and read PEP 8.

@slivingston

Copy link
Copy Markdown
Member

@slivingston Are you OK with this version?

@murrayrm I think this parameter name is good, but there were documentation and whitespace errors that I just requested to change. If you are OK with it, after those are fixed, I will merge this.

@CNZHM666

Copy link
Copy Markdown
Contributor Author

@slivingston Updated the docstrings to explicitly name scipy.linalg.issymmetric and scipy.linalg.ishermitian, and cleaned up the unnecessary blank lines and whitespace changes. The relevant checks pass.

@murrayrm murrayrm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks OK except for one unaddressed comment from @slivingston. OK to merge once @slivingston signs off.

Comment thread control/mateqn.py Outdated
@CNZHM666

CNZHM666 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

@murrayrm Fixed the remaining indentation issue and cleaned up the requested documentation/whitespace changes. The latest changes have been pushed.

@CNZHM666
CNZHM666 requested a review from murrayrm September 25, 2026 00:29

@slivingston slivingston left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CNZHM666 There are still problems with whitespace. Please take some time before requesting review to check this. It is trivial, yet it is repeatedly a problem with your code. Be more careful.

Comment thread control/tests/stochsys_test.py
Comment thread control/tests/stochsys_test.py
Comment thread control/tests/stochsys_test.py
@slivingston

Copy link
Copy Markdown
Member

@CNZHM666 Can you enable code changes to this PR from project maintainers (like me)? If I can just fix the whitespace problems on my own, then we can merge this simple PR.

https://docs.github.com/en/pull-requests/how-tos/work-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork

@CNZHM666

Copy link
Copy Markdown
Contributor Author

@slivingston I already have “Allow edits from maintainers” enabled for this PR. Please feel free to fix the remaining whitespace issues directly. Thanks for your time and for helping me improve this PR.

@slivingston
slivingston merged commit b4cf736 into python-control:main Sep 29, 2026
@slivingston

Copy link
Copy Markdown
Member

@CNZHM666 Thanks for contributing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants