diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index 590d4a97f..50dd8bd22 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -27,7 +27,8 @@ jobs: - name: Install full dependencies shell: bash -l {0} run: | - mamba install cvxopt pandas slycot + mamba install cvxopt pandas + pip install 'slicot>=1.0.12' - name: Run doctest shell: bash -l {0} diff --git a/.github/workflows/install_examples.yml b/.github/workflows/install_examples.yml index 6893a99fb..18056b5c1 100644 --- a/.github/workflows/install_examples.yml +++ b/.github/workflows/install_examples.yml @@ -20,8 +20,9 @@ jobs: --quiet --yes \ python=3.12 pip \ numpy matplotlib scipy \ - slycot pmw jupyter \ + pmw jupyter \ ipython!=9.0 + conda run -n control-examples-env pip install 'slicot>=1.0.12' - name: Install from source run: | diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml index 0aabf33bf..937c6ebce 100644 --- a/.github/workflows/python-package-conda.yml +++ b/.github/workflows/python-package-conda.yml @@ -6,7 +6,7 @@ jobs: test-linux-conda: name: > Py${{ matrix.python-version }}; - ${{ matrix.slycot || 'no' }} Slycot; + ${{ matrix.slycot && 'slycot' || matrix.slicot && 'slicot' || 'no-slicot' }}; ${{ matrix.pandas || 'no' }} Pandas; ${{ matrix.cvxopt || 'no' }} CVXOPT ${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }} @@ -18,6 +18,7 @@ jobs: matrix: python-version: ['3.10', '3.12'] slycot: ["", "conda"] + slicot: [""] pandas: [""] cvxopt: ["", "conda"] mplbackend: [""] @@ -27,6 +28,15 @@ jobs: pandas: conda cvxopt: conda mplbackend: QtAgg + # Test with slicot (pip) instead of slycot (conda) + - python-version: '3.12' + slicot: pip + slycot: "" + cvxopt: conda + exclude: + # Don't test both slycot and slicot together + - slycot: "conda" + slicot: "pip" steps: - uses: actions/checkout@v3 @@ -52,6 +62,9 @@ jobs: if [[ '${{matrix.slycot}}' == 'conda' ]]; then mamba install slycot fi + if [[ '${{matrix.slicot}}' == 'pip' ]]; then + pip install 'slicot>=1.0.12' + fi if [[ '${{matrix.pandas}}' == 'conda' ]]; then mamba install pandas fi @@ -70,7 +83,7 @@ jobs: - name: report coverage uses: coverallsapp/github-action@v2 with: - flag-name: conda-pytest_py${{ matrix.python-version }}_${{ matrix.slycot || 'no' }}-Slycot_${{ matrix.pandas || 'no' }}-Pandas_${{ matrix.cvxopt || 'no' }}_CVXOPT-${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }} + flag-name: conda-pytest_py${{ matrix.python-version }}_${{ matrix.slycot && 'slycot' || matrix.slicot && 'slicot' || 'no-slicot' }}_${{ matrix.pandas || 'no' }}-Pandas_${{ matrix.cvxopt || 'no' }}_CVXOPT-${{ matrix.mplbackend && format('; {0}', matrix.mplbackend) }} parallel: true file: coverage.xml diff --git a/control/canonical.py b/control/canonical.py index 48fda7f5a..a152179b4 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -10,7 +10,7 @@ from numpy.linalg import matrix_rank, solve from scipy.linalg import schur -from .exception import ControlNotImplemented, ControlSlycot +from .exception import ControlNotImplemented, ControlSlicot from .iosys import issiso from .statefbk import ctrb, obsv from .statesp import StateSpace, _convert_to_statespace @@ -330,15 +330,15 @@ def _bdschur_condmax_search(aschur, tschur, condmax): Notes ----- - Outputs as for slycot.mb03rd. + Outputs as for slicot.mb03rd. `aschur`, `tschur` are as returned by scipy.linalg.schur. """ try: - from slycot import mb03rd + from .slicot_compat import mb03rd except ImportError: - raise ControlSlycot("can't find slycot module 'mb03rd'") + raise ControlSlicot("can't find slicot module 'mb03rd'") # see notes on RuntimeError below pmaxlower = None diff --git a/control/exception.py b/control/exception.py index 69b140203..c631170c4 100644 --- a/control/exception.py +++ b/control/exception.py @@ -5,10 +5,28 @@ """Exception definitions for the control package.""" -class ControlSlycot(ImportError): - """Slycot import failed.""" +import warnings + + +class ControlSlicot(ImportError): + """Slicot import failed.""" pass + +def _deprecated_alias(old_name, new_name): + """Issue deprecation warning for renamed class/function.""" + warnings.warn( + f"{old_name} is deprecated, use {new_name} instead", + DeprecationWarning, stacklevel=3 + ) + + +class ControlSlycot(ControlSlicot): + """Deprecated alias for ControlSlicot.""" + def __init__(self, *args, **kwargs): + _deprecated_alias('ControlSlycot', 'ControlSlicot') + super().__init__(*args, **kwargs) + class ControlDimension(ValueError): """Raised when dimensions of system objects are not correct.""" pass @@ -29,18 +47,26 @@ class ControlNotImplemented(NotImplementedError): """Functionality is not yet implemented.""" pass -# Utility function to see if Slycot is installed -slycot_installed = None -def slycot_check(): - """Return True if Slycot is installed, otherwise False.""" - global slycot_installed - if slycot_installed is None: +# Utility function to see if slicot or slycot is installed +slicot_installed = None +def slicot_check(): + """Return True if slicot or slycot is installed, otherwise False.""" + global slicot_installed + if slicot_installed is None: try: - import slycot # noqa: F401 - slycot_installed = True - except: - slycot_installed = False - return slycot_installed + import slicot # noqa: F401 + slicot_installed = True + except ImportError: + try: + import slycot # noqa: F401 + slicot_installed = True + except ImportError: + slicot_installed = False + return slicot_installed + + +# Backwards-compatible alias (no warning to avoid noise in existing code) +slycot_check = slicot_check # Utility function to see if pandas is installed diff --git a/control/margins.py b/control/margins.py index 57e825c65..f1a8b78b8 100644 --- a/control/margins.py +++ b/control/margins.py @@ -16,7 +16,7 @@ from .iosys import issiso from .ctrlutil import mag2db try: - from slycot import ab13md + from .slicot_compat import ab13md except ImportError: ab13md = None @@ -577,7 +577,7 @@ def disk_margins(L, omega, skew=0.0, returnall=False): # Need slycot if L is MIMO, for mu calculation if not L.issiso() and ab13md == None: raise ControlMIMONotImplemented( - "Need slycot to compute MIMO disk_margins") + "Need slicot to compute MIMO disk_margins") # Get dimensions of feedback system num_loops = statesp.ss(L).C.shape[0] diff --git a/control/mateqn.py b/control/mateqn.py index 9d1349b0c..349db06b2 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -17,42 +17,34 @@ from numpy import eye, finfo, inexact from scipy.linalg import eigvals, solve -from .exception import ControlArgument, ControlDimension, ControlSlycot, \ - slycot_check +from .exception import ControlArgument, ControlDimension, ControlSlicot, \ + slicot_check -# Make sure we have access to the right Slycot routines +# Make sure we have access to the right slicot routines try: - from slycot.exceptions import SlycotResultWarning + from .slicot_compat import SlicotResultWarning except ImportError: - SlycotResultWarning = UserWarning + SlicotResultWarning = UserWarning try: - from slycot import sb03md57 - - # wrap without the deprecation warning - def sb03md(n, C, A, U, dico, job='X', fact='N', trana='N', ldwork=None): - ret = sb03md57(A, U, C, dico, job, fact, trana, ldwork) - return ret[2:] + from .slicot_compat import sb03md except ImportError: - try: - from slycot import sb03md - except ImportError: - sb03md = None + sb03md = None try: - from slycot import sb04md + from .slicot_compat import sb04md except ImportError: sb04md = None try: - from slycot import sb04qd + from .slicot_compat import sb04qd except ImportError: - sb0qmd = None + sb04qd = None try: - from slycot import sg03ad + from .slicot_compat import sg03ad except ImportError: - sb04ad = None + sg03ad = None __all__ = ['lyap', 'dlyap', 'dare', 'care'] @@ -95,7 +87,7 @@ def lyap(A, Q, C=None, E=None, method=None): If present, solve the generalized Lyapunov equation. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -105,12 +97,12 @@ def lyap(A, Q, C=None, E=None, method=None): """ # Decide what method to use - method = _slycot_or_scipy(method) - if method == 'slycot': + method = _slicot_or_scipy(method) + if method == 'slicot': if sb03md is None: - raise ControlSlycot("Can't find slycot module 'sb03md'") + raise ControlSlicot("Can't find slicot module 'sb03md'") if sb04md is None: - raise ControlSlycot("Can't find slycot module 'sb04md'") + raise ControlSlicot("Can't find slicot module 'sb04md'") # Reshape input arrays A = np.array(A, ndmin=2) @@ -136,9 +128,9 @@ def lyap(A, Q, C=None, E=None, method=None): # Solve the Lyapunov equation using SciPy return sp.linalg.solve_continuous_lyapunov(A, -Q) - # Solve the Lyapunov equation by calling Slycot function sb03md + # Solve the Lyapunov equation by calling slicot function sb03md with warnings.catch_warnings(): - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) X, scale, sep, ferr, w = \ sb03md(n, -Q, A, eye(n, n), 'C', trana='T') @@ -152,7 +144,7 @@ def lyap(A, Q, C=None, E=None, method=None): # Solve the Sylvester equation using SciPy return sp.linalg.solve_sylvester(A, Q, -C) - # Solve the Sylvester equation by calling the Slycot function sb04md + # Solve the Sylvester equation by calling the slicot function sb04md X = sb04md(n, m, A, Q, -C) # Solve the generalized Lyapunov equation @@ -165,17 +157,14 @@ def lyap(A, Q, C=None, E=None, method=None): raise ControlArgument( "method='scipy' not valid for generalized Lyapunov equation") - # Make sure we have access to the write Slycot routine - try: - from slycot import sg03ad - - except ImportError: - raise ControlSlycot("Can't find slycot module 'sg03ad'") + # Make sure we have access to the right slicot routine + if sg03ad is None: + raise ControlSlicot("Can't find slicot module 'sg03ad'") - # Solve the generalized Lyapunov equation by calling Slycot + # Solve the generalized Lyapunov equation by calling slicot # function sg03ad with warnings.catch_warnings(): - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) A, E, Q, Z, X, scale, sep, ferr, alphar, alphai, beta = \ sg03ad('C', 'B', 'N', 'T', 'L', n, A, E, eye(n, n), eye(n, n), -Q) @@ -221,7 +210,7 @@ def dlyap(A, Q, C=None, E=None, method=None): If present, solve the generalized Lyapunov equation. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -231,16 +220,16 @@ def dlyap(A, Q, C=None, E=None, method=None): """ # Decide what method to use - method = _slycot_or_scipy(method) + method = _slicot_or_scipy(method) - if method == 'slycot': - # Make sure we have access to the right slycot routines + if method == 'slicot': + # Make sure we have access to the right slicot routines if sb03md is None: - raise ControlSlycot("Can't find slycot module 'sb03md'") + raise ControlSlicot("Can't find slicot module 'sb03md'") if sb04qd is None: - raise ControlSlycot("Can't find slycot module 'sb04qd'") + raise ControlSlicot("Can't find slicot module 'sb04qd'") if sg03ad is None: - raise ControlSlycot("Can't find slycot module 'sg03ad'") + raise ControlSlicot("Can't find slicot module 'sg03ad'") # Reshape input arrays A = np.array(A, ndmin=2) @@ -266,9 +255,9 @@ def dlyap(A, Q, C=None, E=None, method=None): # Solve the Lyapunov equation using SciPy return sp.linalg.solve_discrete_lyapunov(A, Q) - # Solve the Lyapunov equation by calling the Slycot function sb03md + # Solve the Lyapunov equation by calling the slicot function sb03md with warnings.catch_warnings(): - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) X, scale, sep, ferr, w = \ sb03md(n, -Q, A, eye(n, n), 'D', trana='T') @@ -282,7 +271,7 @@ def dlyap(A, Q, C=None, E=None, method=None): raise ControlArgument( "method='scipy' not valid for Sylvester equation") - # Solve the Sylvester equation by calling Slycot function sb04qd + # Solve the Sylvester equation by calling slicot function sb04qd X = sb04qd(n, m, -A, Q.T, C) # Solve the generalized Lyapunov equation @@ -295,10 +284,10 @@ def dlyap(A, Q, C=None, E=None, method=None): raise ControlArgument( "method='scipy' not valid for generalized Lyapunov equation") - # Solve the generalized Lyapunov equation by calling Slycot + # Solve the generalized Lyapunov equation by calling slicot # function sg03ad with warnings.catch_warnings(): - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) A, E, Q, Z, X, scale, sep, ferr, alphar, alphai, beta = \ sg03ad('D', 'B', 'N', 'T', 'L', n, A, E, eye(n, n), eye(n, n), -Q) @@ -347,10 +336,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, Input matrices for generalized Riccati equation. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. stabilizing : bool, optional - If `method` is 'slycot', unstabilized eigenvalues will be returned + If `method` is 'slicot', unstabilized eigenvalues will be returned in the initial elements of `L`. Not supported for 'scipy'. Returns @@ -364,7 +353,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, """ # Decide what method to use - method = _slycot_or_scipy(method) + method = _slicot_or_scipy(method) # Reshape input arrays A = np.array(A, ndmin=2) @@ -399,18 +388,18 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, E, _ = np.linalg.eig(A - B @ K) return X, E, K - # Make sure we can import required Slycot routines + # Make sure we can import required slicot routines try: - from slycot import sb02md + from .slicot_compat import sb02md except ImportError: - raise ControlSlycot("Can't find slycot module 'sb02md'") + raise ControlSlicot("Can't find slicot module 'sb02md'") try: - from slycot import sb02mt + from .slicot_compat import sb02mt except ImportError: - raise ControlSlycot("Can't find slycot module 'sb02mt'") + raise ControlSlicot("Can't find slicot module 'sb02mt'") - # Solve the standard algebraic Riccati equation by calling Slycot + # Solve the standard algebraic Riccati equation by calling slicot # functions sb02mt and sb02md A_b, B_b, Q_b, R_b, L_b, ipiv, oufact, G = sb02mt(n, m, B, R) @@ -445,17 +434,17 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, eigs, _ = sp.linalg.eig(A - B @ K, E) return X, eigs, K - # Make sure we can find the required Slycot routine + # Make sure we can find the required slicot routine try: - from slycot import sg02ad + from .slicot_compat import sg02ad except ImportError: - raise ControlSlycot("Can't find slycot module sg02ad") + raise ControlSlicot("Can't find slicot module sg02ad") # Solve the generalized algebraic Riccati equation by calling the - # Slycot function sg02ad + # slicot function sg02ad with warnings.catch_warnings(): sort = 'S' if stabilizing else 'U' - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) rcondu, X, alfar, alfai, beta, S_o, T, U, iwarn = \ sg02ad('C', 'B', 'N', 'U', 'N', 'N', sort, 'R', n, m, 0, A, E, B, Q, R, S) @@ -503,10 +492,10 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, Input matrices for generalized Riccati equation. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. stabilizing : bool, optional - If `method` is 'slycot', unstabilized eigenvalues will be returned + If `method` is 'slicot', unstabilized eigenvalues will be returned in the initial elements of `L`. Not supported for 'scipy'. Returns @@ -520,7 +509,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, """ # Decide what method to use - method = _slycot_or_scipy(method) + method = _slicot_or_scipy(method) # Reshape input arrays A = np.array(A, ndmin=2) @@ -564,21 +553,21 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, return X, L, G - # Make sure we can import required Slycot routine + # Make sure we can import required slicot routine try: - from slycot import sg02ad + from .slicot_compat import sg02ad except ImportError: - raise ControlSlycot("Can't find slycot module sg02ad") + raise ControlSlicot("Can't find slicot module sg02ad") # Initialize optional matrices S = np.zeros((n, m)) if S is None else np.array(S, ndmin=2) E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2) # Solve the generalized algebraic Riccati equation by calling the - # Slycot function sg02ad + # slicot function sg02ad sort = 'S' if stabilizing else 'U' with warnings.catch_warnings(): - warnings.simplefilter("error", category=SlycotResultWarning) + warnings.simplefilter("error", category=SlicotResultWarning) rcondu, X, alfar, alfai, beta, S_o, T, U, iwarn = \ sg02ad('D', 'B', 'N', 'U', 'N', 'N', sort, 'R', n, m, 0, A, E, B, Q, R, S) @@ -595,10 +584,10 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, # Utility function to decide on method to use -def _slycot_or_scipy(method): - if method == 'slycot' or (method is None and slycot_check()): - return 'slycot' - elif method == 'scipy' or (method is None and not slycot_check()): +def _slicot_or_scipy(method): + if method == 'slicot' or (method is None and slicot_check()): + return 'slicot' + elif method == 'scipy' or (method is None and not slicot_check()): return 'scipy' else: raise ControlArgument("Unknown method %s" % method) diff --git a/control/modelsimp.py b/control/modelsimp.py index 3352cc156..c4d7c4a9b 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -15,7 +15,7 @@ # External packages and modules import numpy as np -from .exception import ControlArgument, ControlDimension, ControlSlycot +from .exception import ControlArgument, ControlDimension, ControlSlicot from .iosys import isctime, isdtime from .statefbk import gram from .statesp import StateSpace @@ -295,7 +295,7 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): ValueError If `method` is not 'truncate' or 'matchdc'. ImportError - If slycot routine ab09ad, ab09md, or ab09nd is not found. + If slicot routine ab09ad, ab09md, or ab09nd is not found. ValueError If there are more unstable modes than any value in orders. @@ -311,15 +311,15 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): raise ValueError("supported methods are 'truncate' or 'matchdc'") elif method == 'truncate': try: - from slycot import ab09ad, ab09md + from .slicot_compat import ab09ad, ab09md except ImportError: - raise ControlSlycot( - "can't find slycot subroutine ab09md or ab09ad") + raise ControlSlicot( + "can't find slicot subroutine ab09md or ab09ad") elif method == 'matchdc': try: - from slycot import ab09nd + from .slicot_compat import ab09nd except ImportError: - raise ControlSlycot("can't find slycot subroutine ab09nd") + raise ControlSlicot("can't find slicot subroutine ab09nd") # Check for ss system object, need a utility for this? diff --git a/control/robust.py b/control/robust.py index 197222390..541383fdc 100644 --- a/control/robust.py +++ b/control/robust.py @@ -10,7 +10,7 @@ # External packages and modules import numpy as np -from .exception import ControlSlycot +from .exception import ControlSlicot from .statesp import StateSpace @@ -34,7 +34,7 @@ def h2syn(P, nmeas, ncon): Raises ------ ImportError - If slycot routine sb10hd is not loaded. + If slicot routine sb10hd is not loaded. See Also -------- @@ -67,9 +67,9 @@ def h2syn(P, nmeas, ncon): # TODO: Check for continous or discrete, only continuous supported right now try: - from slycot import sb10hd + from .slicot_compat import sb10hd except ImportError: - raise ControlSlycot("can't find slycot subroutine sb10hd") + raise ControlSlicot("can't find slicot subroutine sb10hd") n = np.size(P.A, 0) m = np.size(P.B, 1) @@ -116,7 +116,7 @@ def hinfsyn(P, nmeas, ncon): Raises ------ ImportError - If slycot routine sb10ad is not loaded. + If slicot routine sb10ad is not loaded. See Also -------- @@ -149,9 +149,9 @@ def hinfsyn(P, nmeas, ncon): # TODO: Check for continous or discrete, only continuous supported right now try: - from slycot import sb10ad + from .slicot_compat import sb10ad except ImportError: - raise ControlSlycot("can't find slycot subroutine sb10ad") + raise ControlSlicot("can't find slicot subroutine sb10ad") n = np.size(P.A, 0) m = np.size(P.B, 1) diff --git a/control/slicot_compat.py b/control/slicot_compat.py new file mode 100644 index 000000000..9a989b079 --- /dev/null +++ b/control/slicot_compat.py @@ -0,0 +1,1143 @@ +# slicot_compat.py - compatibility wrappers for slicot/slycot packages +# +# This module provides wrappers around the slicot package functions to match +# the API used by the older slycot package. It also supports falling back to +# slycot if slicot is not installed. + +"""Compatibility layer for slicot/slycot packages. + +This module wraps slicot functions to match the slycot API, minimizing changes +to existing code in python-control. If slicot is not installed, it falls back +to using slycot directly. +""" + +import numpy as np + +from .exception import ControlArgument + +# Try to import slicot first (preferred), fall back to slycot +_use_slicot = False +_use_slycot = False + +try: + import slicot # noqa: F401 + _use_slicot = True +except ImportError: + try: + import slycot # noqa: F401 + _use_slycot = True + except ImportError: + pass + +if not _use_slicot and not _use_slycot: + raise ImportError("Neither slicot nor slycot is installed") + +__all__ = [ + 'SlicotResultWarning', 'SlicotArithmeticError', + 'sb03md', 'sb03od', 'sb04md', 'sb04qd', 'sg03ad', + 'sb02md', 'sb02mt', 'sg02ad', 'sb01bd', + 'sb10ad', 'sb10hd', 'ab08nd', + 'ab09ad', 'ab09md', 'ab09nd', + 'ab13bd', 'ab13dd', 'ab13md', + 'tb01pd', 'tb04ad', 'tb05ad', 'td04ad', 'mb03rd', +] + + +class SlicotResultWarning(UserWarning): + """Warning for non-fatal issues from SLICOT routines.""" + pass + + +class SlicotArithmeticError(ArithmeticError): + """Error for arithmetic failures in SLICOT routines.""" + + def __init__(self, message, info=0): + super().__init__(message) + self.info = info + + +def _check_info(info, routine_name, warn_codes=None): + """Check info code and raise appropriate exception. + + Parameters + ---------- + info : int + Info code returned by SLICOT routine. + routine_name : str + Name of the routine for error messages. + warn_codes : list of int, optional + Info codes that should generate warnings instead of errors. + """ + if info == 0: + return + if warn_codes and info in warn_codes: + import warnings + warnings.warn( + f"{routine_name} returned info={info}", + SlicotResultWarning + ) + return + if info < 0: + raise ControlArgument(f"{routine_name}: parameter {-info} is invalid") + raise SlicotArithmeticError( + f"{routine_name} returned info={info}", info=info + ) + + +def sb03md(n, C, A, U, dico, job='X', fact='N', trana='N', ldwork=None): + """Solve Lyapunov equation (slycot-compatible wrapper). + + slycot API: X, scale, sep, ferr, w = sb03md(n, C, A, U, dico, job, fact, trana) + slicot API: x, a, u, wr, wi, scale, sep, ferr, info = + sb03md(dico, job, fact, trana, n, a, c, u) + + Returns + ------- + X : ndarray + Solution matrix. + scale : float + Scale factor. + sep : float + Separation estimate. + ferr : float + Forward error bound. + w : ndarray + Eigenvalues of A (complex). + """ + from slicot import sb03md as _sb03md + + A_copy = np.asfortranarray(A.copy()) + C_copy = np.asfortranarray(C.copy()) + U_copy = np.asfortranarray(U.copy()) if fact == 'F' else None + + if fact == 'F': + X, A_out, U_out, wr, wi, scale, sep, ferr, info = _sb03md( + dico, job, fact, trana, n, A_copy, C_copy, U_copy + ) + else: + X, A_out, U_out, wr, wi, scale, sep, ferr, info = _sb03md( + dico, job, fact, trana, n, A_copy, C_copy + ) + + _check_info(info, 'sb03md', warn_codes=[1, 2]) + + w = wr + 1j * wi + + return X, scale, sep, ferr, w + + +def sb03od(n, m, A, Q, B, dico, fact='N', trans='N', ldwork=None): + """Solve Lyapunov equation with Cholesky factor (slycot-compatible wrapper). + + slycot API: X, scale, w = sb03od(n, m, A, Q, B, dico, fact, trans) + slicot API: u, q_out, wr, wi, scale, info = sb03od(dico, fact, trans, a, b, [q]) + + Returns + ------- + X : ndarray + Cholesky factor of solution. + scale : float + Scale factor. + w : ndarray + Eigenvalues of A (complex). + """ + from slicot import sb03od as _sb03od + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + + if fact == 'F': + Q_copy = np.asfortranarray(Q.copy()) + u, q_out, wr, wi, scale, info = _sb03od( + dico, fact, trans, A_copy, B_copy, Q_copy + ) + else: + u, q_out, wr, wi, scale, info = _sb03od( + dico, fact, trans, A_copy, B_copy + ) + + _check_info(info, 'sb03od') + + w = wr + 1j * wi + + return u, scale, w + + +def sb04md(n, m, A, B, C, ldwork=None): + """Solve Sylvester equation AX + XB = C (slycot-compatible wrapper). + + slycot API: X = sb04md(n, m, A, B, C) + slicot API: x, z, info = sb04md(a, b, c) + + Returns + ------- + X : ndarray + Solution matrix. + """ + from slicot import sb04md as _sb04md + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + + X, Z, info = _sb04md(A_copy, B_copy, C_copy) + + _check_info(info, 'sb04md') + + return X + + +def sb04qd(n, m, A, B, C, ldwork=None): + """Solve discrete Sylvester equation AXB + X = C (slycot-compatible wrapper). + + slycot API: X = sb04qd(n, m, A, B, C) + slicot API: x, z, info = sb04qd(a, b, c) + + Returns + ------- + X : ndarray + Solution matrix. + """ + from slicot import sb04qd as _sb04qd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + + X, Z, info = _sb04qd(A_copy, B_copy, C_copy) + + _check_info(info, 'sb04qd') + + return X + + +def sg03ad(dico, job, fact, trana, uplo, n, A, E, Q, Z, B, ldwork=None): + """Solve generalized Lyapunov equation (slycot-compatible wrapper). + + slycot API: A, E, Q, Z, X, scale, sep, ferr, alphar, alphai, beta = + sg03ad(dico, job, fact, trana, uplo, n, A, E, Q, Z, B) + slicot API: x, scale, sep, ferr, alphar, alphai, beta, a, e, q, z, info = + sg03ad(dico, job, fact, trans, uplo, n, a, e, x, q, z) + + Returns + ------- + A : ndarray + Updated A matrix (Schur form). + E : ndarray + Updated E matrix. + Q : ndarray + Orthogonal transformation Q. + Z : ndarray + Orthogonal transformation Z. + X : ndarray + Solution matrix. + scale : float + Scale factor. + sep : float + Separation estimate. + ferr : float + Forward error bound. + alphar, alphai, beta : ndarray + Generalized eigenvalues. + """ + from slicot import sg03ad as _sg03ad + + A_copy = np.asfortranarray(A.copy()) + E_copy = np.asfortranarray(E.copy()) + B_copy = np.asfortranarray(B.copy()) + + if fact == 'F': + Q_copy = np.asfortranarray(Q.copy()) + Z_copy = np.asfortranarray(Z.copy()) + X, scale, sep, ferr, alphar, alphai, beta, A_out, E_out, Q_out, Z_out, info = _sg03ad( + dico, job, fact, trana, uplo, n, A_copy, E_copy, B_copy, Q_copy, Z_copy + ) + else: + X, scale, sep, ferr, alphar, alphai, beta, A_out, E_out, Q_out, Z_out, info = _sg03ad( + dico, job, fact, trana, uplo, n, A_copy, E_copy, B_copy + ) + + _check_info(info, 'sg03ad', warn_codes=[1, 2, 3]) + + return A_out, E_out, Q_out, Z_out, X, scale, sep, ferr, alphar, alphai, beta + + +def sb02md(n, A, G, Q, dico, hinv='D', uplo='U', scal='N', sort='S', ldwork=None): + """Solve algebraic Riccati equation (slycot-compatible wrapper). + + slycot API: X, rcond, w, S, U, A_inv = sb02md(n, A, G, Q, dico, hinv, uplo, scal, sort) + slicot API: X, rcond, wr, wi, S, U, info = sb02md(dico, hinv, uplo, scal, sort, n, A, G, Q) + + Returns + ------- + X : ndarray + Solution matrix. + rcond : float + Reciprocal condition number. + w : ndarray + Closed-loop eigenvalues (complex). + S : ndarray + Schur form. + U : ndarray + Orthogonal transformation. + A_inv : ndarray + Inverse of A (if computed). + """ + from slicot import sb02md as _sb02md + + A_copy = np.asfortranarray(A.copy()) + G_copy = np.asfortranarray(G.copy()) + Q_copy = np.asfortranarray(Q.copy()) + + X, rcond, wr, wi, S, U, info = _sb02md( + dico, hinv, uplo, scal, sort, n, A_copy, G_copy, Q_copy + ) + + _check_info(info, 'sb02md') + + w = wr + 1j * wi + A_inv = A_copy + + return X, rcond, w, S, U, A_inv + + +def sb02mt(n, m, B, R, A=None, Q=None, L=None, fact='N', jobl='Z', uplo='U', ldwork=None): + """Prepare data for Riccati solver (slycot-compatible wrapper). + + slycot API: A_b, B_b, Q_b, R_b, L_b, ipiv, oufact, G = sb02mt(n, m, B, R, ...) + slicot API (jobg='G', jobl='Z'): G, oufact, info = sb02mt(...) + + Returns + ------- + A_b : ndarray + Input matrix A (unchanged). + B_b : ndarray + Input matrix B (unchanged). + Q_b : ndarray + Input matrix Q (unchanged). + R_b : ndarray + Factored R matrix. + L_b : ndarray + Cross-weighting matrix. + ipiv : ndarray + Pivot indices (empty for slicot). + oufact : int + Output factorization flag. + G : ndarray + G = B * inv(R) * B'. + """ + from slicot import sb02mt as _sb02mt + + if A is None: + A = np.zeros((n, n), dtype=float, order='F') + if Q is None: + Q = np.zeros((n, n), dtype=float, order='F') + if L is None: + L = np.zeros((n, m), dtype=float, order='F') + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + Q_copy = np.asfortranarray(Q.copy()) + R_copy = np.asfortranarray(R.copy()) + L_copy = np.asfortranarray(L.copy()) + + G_out = np.zeros((n, n), dtype=float, order='F') + + if jobl == 'Z': + G, oufact, info = _sb02mt( + 'G', jobl, fact, uplo, n, m, A_copy, B_copy, Q_copy, R_copy, L_copy, G_out + ) + ipiv = np.array([], dtype=np.int32) + else: + A_out, B_out, Q_out, L_out, G, oufact, info = _sb02mt( + 'G', jobl, fact, uplo, n, m, A_copy, B_copy, Q_copy, R_copy, L_copy, G_out + ) + A_copy, B_copy, Q_copy, L_copy = A_out, B_out, Q_out, L_out + ipiv = np.array([], dtype=np.int32) + + _check_info(info, 'sb02mt') + + return A_copy, B_copy, Q_copy, R_copy, L_copy, ipiv, oufact, G + + +def sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, + A, E, B, Q, R, L, ldwork=None, tol=0.0): + """Solve generalized Riccati equation (slycot-compatible wrapper). + + slycot API: rcondu, X, alfar, alfai, beta, S, T, U, iwarn = + sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A, E, B, Q, R, L) + slicot API: X, rcondu, alfar, alfai, beta, S, T, U, iwarn, info = + sg02ad(dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, A, E, B, Q, R, L, tol) + + Returns + ------- + rcondu : float + Reciprocal condition number. + X : ndarray + Solution matrix. + alfar, alfai, beta : ndarray + Generalized eigenvalues. + S : ndarray + Schur form. + T : ndarray + Triangular factor. + U : ndarray + Orthogonal transformation. + iwarn : int + Warning indicator. + """ + from slicot import sg02ad as _sg02ad + + A_copy = np.asfortranarray(A.copy()) + E_copy = np.asfortranarray(E.copy()) + B_copy = np.asfortranarray(B.copy()) + Q_copy = np.asfortranarray(Q.copy()) + R_copy = np.asfortranarray(R.copy()) + L_copy = np.asfortranarray(L.copy()) + + X, rcondu, alfar, alfai, beta, S, T, U, iwarn, info = _sg02ad( + dico, jobb, fact, uplo, jobl, scal, sort, acc, n, m, p, + A_copy, E_copy, B_copy, Q_copy, R_copy, L_copy, tol + ) + + _check_info(info, 'sg02ad', warn_codes=[1, 2]) + + return rcondu, X, alfar, alfai, beta, S, T, U, iwarn + + +def sb01bd(n, m, np_, alpha, A, B, w, dico, tol=0.0, ldwork=None): + """Pole placement via Varga method (slycot-compatible wrapper). + + slycot API: A_z, w_out, nfp, nap, nup, F, Z = sb01bd(n, m, np, alpha, A, B, w, dico) + slicot API: a, wr, wi, nfp, nap, nup, F, Z, iwarn, info = + sb01bd(dico, n, m, np, alpha, a, b, wr, wi, tol) + + Parameters + ---------- + n : int + State dimension. + m : int + Input dimension. + np_ : int + Number of eigenvalues to assign. + alpha : float + Threshold for fixed eigenvalues. + A : ndarray + State matrix. + B : ndarray + Input matrix. + w : ndarray + Desired eigenvalues (complex array). + dico : str + 'C' for continuous, 'D' for discrete. + + Returns + ------- + A_z : ndarray + Modified A matrix. + w_out : ndarray + Assigned eigenvalues (complex). + nfp : int + Number of fixed poles. + nap : int + Number of assigned poles. + nup : int + Number of uncontrollable poles. + F : ndarray + Feedback gain matrix. + Z : ndarray + Orthogonal transformation. + """ + from slicot import sb01bd as _sb01bd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + wr = np.asfortranarray(np.real(w).copy()) + wi = np.asfortranarray(np.imag(w).copy()) + + a_out, wr_out, wi_out, nfp, nap, nup, F, Z, iwarn, info = _sb01bd( + dico, n, m, np_, alpha, A_copy, B_copy, wr, wi, tol + ) + + _check_info(info, 'sb01bd', warn_codes=[1, 2, 3]) + + w_out = wr_out + 1j * wi_out + + return a_out, w_out, nfp, nap, nup, F, Z + + +def sb10ad(n, m, np_, ncon, nmeas, gamma, A, B, C, D, ldwork=None, + job=1, gtol=0.0, actol=0.0): + """H-infinity controller synthesis (slycot-compatible wrapper). + + slycot API: Ak, Bk, Ck, Dk, Ac, Bc, Cc, Dc, rcond = + sb10ad(n, m, np, ncon, nmeas, gamma, A, B, C, D) + slicot API: Ak, Bk, Ck, Dk, Ac, Bc, Cc, Dc, gamma_out, rcond, info = + sb10ad(job, n, m, np, ncon, nmeas, A, B, C, D, gamma, gtol, actol) + + Returns + ------- + Ak, Bk, Ck, Dk : ndarray + Controller state-space matrices. + Ac, Bc, Cc, Dc : ndarray + Closed-loop system matrices. + rcond : ndarray + Reciprocal condition numbers. + """ + from slicot import sb10ad as _sb10ad + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + Ak, Bk, Ck, Dk, Ac, Bc, Cc, Dc, gamma_out, rcond, info = _sb10ad( + job, n, m, np_, ncon, nmeas, A_copy, B_copy, C_copy, D_copy, gamma, gtol, actol + ) + + _check_info(info, 'sb10ad') + + return gamma_out, Ak, Bk, Ck, Dk, Ac, Bc, Cc, Dc, rcond + + +def sb10hd(n, m, np_, ncon, nmeas, A, B, C, D, ldwork=None, tol=0.0): + """H2 controller synthesis (slycot-compatible wrapper). + + slycot API: Ak, Bk, Ck, Dk, rcond = sb10hd(n, m, np, ncon, nmeas, A, B, C, D) + slicot API: Ak, Bk, Ck, Dk, rcond, info = sb10hd(n, m, np, ncon, nmeas, A, B, C, D, tol) + + Returns + ------- + Ak, Bk, Ck, Dk : ndarray + Controller state-space matrices. + rcond : ndarray + Reciprocal condition numbers. + """ + from slicot import sb10hd as _sb10hd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + Ak, Bk, Ck, Dk, rcond, info = _sb10hd( + n, m, np_, ncon, nmeas, A_copy, B_copy, C_copy, D_copy, tol + ) + + _check_info(info, 'sb10hd') + + return Ak, Bk, Ck, Dk, rcond + + +def ab08nd(n, m, p, A, B, C, D, equil='N', tol=0.0, ldwork=None): + """Compute system zeros (slycot-compatible wrapper). + + slycot API: nu, rank, dinfz, nkror, nkrol, infz, kronr, kronl, Af, Bf = + ab08nd(n, m, p, A, B, C, D, equil, tol) + slicot API: nu, rank, dinfz, nkror, nkrol, infz, kronr, kronl, Af, Bf, info = + ab08nd(equil, n, m, p, A, B, C, D, tol) + + Returns + ------- + nu : int + Number of finite zeros. + rank : int + Rank of system. + dinfz : int + Number of infinite zeros. + nkror : int + Number of right Kronecker indices. + nkrol : int + Number of left Kronecker indices. + infz : ndarray + Infinite zero structure. + kronr : ndarray + Right Kronecker indices. + kronl : ndarray + Left Kronecker indices. + Af : ndarray + Reduced A matrix. + Bf : ndarray + Reduced E matrix. + """ + from slicot import ab08nd as _ab08nd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + nu, rank, dinfz, nkror, nkrol, infz, kronr, kronl, Af, Bf, info = _ab08nd( + equil, n, m, p, A_copy, B_copy, C_copy, D_copy, tol + ) + + _check_info(info, 'ab08nd') + + return nu, rank, dinfz, nkror, nkrol, infz, kronr, kronl, Af, Bf + + +def ab09ad(dico, job, equil, n, m, p, A, B, C, nr=0, tol=0.0, ldwork=None): + """Model reduction via balanced truncation (slycot-compatible wrapper). + + slycot API: Nr, Ar, Br, Cr, hsv = ab09ad(dico, job, equil, n, m, p, A, B, C, nr, tol) + slicot API: ar, br, cr, hsv, nr_out, iwarn, info = ab09ad(dico, job, equil, ordsel, n, m, p, nr, a, b, c, tol) + + Returns + ------- + Nr : int + Order of reduced system. + Ar, Br, Cr : ndarray + Reduced system matrices. + hsv : ndarray + Hankel singular values. + """ + from slicot import ab09ad as _ab09ad + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + + ordsel = 'A' if nr == 0 else 'F' + + Ar_full, Br_full, Cr_full, hsv, Nr_out, iwarn, info = _ab09ad( + dico, job, equil, ordsel, n, m, p, nr, A_copy, B_copy, C_copy, tol + ) + + _check_info(info, 'ab09ad', warn_codes=[1]) + + Ar = Ar_full[:Nr_out, :Nr_out].copy() + Br = Br_full[:Nr_out, :].copy() + Cr = Cr_full[:, :Nr_out].copy() + + return Nr_out, Ar, Br, Cr, hsv + + +def ab09md(dico, job, equil, n, m, p, A, B, C, alpha=0.0, nr=0, tol=0.0, ldwork=None): + """Model reduction for unstable systems (slycot-compatible wrapper). + + slycot API: Nr, Ar, Br, Cr, Ns, hsv = ab09md(dico, job, equil, n, m, p, A, B, C, alpha, nr, tol) + slicot API: ar, br, cr, ns, hsv, nr_out, iwarn, info = ab09md(dico, job, equil, ordsel, n, m, p, nr, alpha, a, b, c, tol) + + Returns + ------- + Nr : int + Order of reduced system. + Ar, Br, Cr : ndarray + Reduced system matrices. + Ns : int + Number of stable eigenvalues. + hsv : ndarray + Hankel singular values. + """ + from slicot import ab09md as _ab09md + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + + ordsel = 'A' if nr == 0 else 'F' + + Ar_full, Br_full, Cr_full, Ns, hsv, Nr_out, iwarn, info = _ab09md( + dico, job, equil, ordsel, n, m, p, nr, alpha, A_copy, B_copy, C_copy, tol + ) + + _check_info(info, 'ab09md', warn_codes=[1]) + + Ar = Ar_full[:Nr_out, :Nr_out].copy() + Br = Br_full[:Nr_out, :].copy() + Cr = Cr_full[:, :Nr_out].copy() + + return Nr_out, Ar, Br, Cr, Ns, hsv + + +def ab09nd(dico, job, equil, n, m, p, A, B, C, D, alpha=0.0, nr=0, + tol1=0.0, tol2=0.0, ldwork=None): + """Model reduction with DC matching (slycot-compatible wrapper). + + slycot API: Nr, Ar, Br, Cr, Dr, Ns, hsv = + ab09nd(dico, job, equil, n, m, p, A, B, C, D, alpha, nr, tol1, tol2) + slicot API: ar, br, cr, dr, nr_out, ns, hsv, iwarn, info = + ab09nd(dico, job, equil, ordsel, n, m, p, nr, alpha, a, b, c, d, tol1, tol2) + + Returns + ------- + Nr : int + Order of reduced system. + Ar, Br, Cr, Dr : ndarray + Reduced system matrices. + Ns : int + Number of stable eigenvalues. + hsv : ndarray + Hankel singular values. + """ + from slicot import ab09nd as _ab09nd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + ordsel = 'A' if nr == 0 else 'F' + + Ar_full, Br_full, Cr_full, Dr_full, Nr_out, Ns, hsv, iwarn, info = _ab09nd( + dico, job, equil, ordsel, n, m, p, nr, alpha, A_copy, B_copy, C_copy, D_copy, tol1, tol2 + ) + + _check_info(info, 'ab09nd', warn_codes=[1, 2]) + + Ar = Ar_full[:Nr_out, :Nr_out].copy() + Br = Br_full[:Nr_out, :].copy() + Cr = Cr_full[:, :Nr_out].copy() + Dr = Dr_full.copy() + + return Nr_out, Ar, Br, Cr, Dr, Ns, hsv + + +def ab13bd(dico, jobn, n, m, p, A, B, C, D, tol=0.0, ldwork=None): + """Compute H2 or L2 norm (slycot-compatible wrapper). + + slycot API: norm = ab13bd(dico, jobn, n, m, p, A, B, C, D, tol) + slicot API: norm, nq, iwarn, info = ab13bd(dico, jobn, A, B, C, D, tol) + + Returns + ------- + norm : float + The H2 or L2 norm. + """ + from slicot import ab13bd as _ab13bd + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + norm, nq, iwarn, info = _ab13bd( + dico, jobn, A_copy, B_copy, C_copy, D_copy, tol + ) + + _check_info(info, 'ab13bd') + + return norm + + +def ab13dd(dico, jobe, equil, jobd, n, m, p, A, E, B, C, D, tol=0.0, ldwork=None): + """Compute L-infinity norm (slycot-compatible wrapper). + + slycot API: gpeak, fpeak = ab13dd(dico, jobe, equil, jobd, n, m, p, A, E, B, C, D, tol) + slicot API: gpeak, fpeak, info = ab13dd(dico, jobe, equil, jobd, n, m, p, fpeak_in, A, E, B, C, D, tol) + + Returns + ------- + gpeak : float + The L-infinity norm. + fpeak : float + Frequency at which peak occurs. + """ + from slicot import ab13dd as _ab13dd + + A_copy = np.asfortranarray(A.copy()) + E_copy = np.asfortranarray(E.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + fpeak_in = np.array([0.0, 1.0], order='F', dtype=float) + + gpeak, fpeak_out, info = _ab13dd( + dico, jobe, equil, jobd, n, m, p, fpeak_in, A_copy, E_copy, B_copy, C_copy, D_copy, tol + ) + + _check_info(info, 'ab13dd') + + return gpeak[0], fpeak_out[0] + + +def ab13md(A, ITYPE, NBLOCK, ldwork=None): + """Compute structured singular value (mu) (slycot-compatible wrapper). + + This is used for disk margin computations. + + slycot API: mu, D, G = ab13md(A, ITYPE, NBLOCK) + slicot API: mu, D, G, info = ab13md(A, ITYPE, NBLOCK) + + Parameters + ---------- + A : ndarray + Complex matrix (frequency response at a single frequency). + ITYPE : ndarray + Integer array specifying block types. + NBLOCK : ndarray + Integer array specifying block sizes. + + Returns + ------- + mu : float + Upper bound on structured singular value. + D : ndarray + D scaling matrix. + G : ndarray + G scaling matrix. + """ + from slicot import ab13md as _ab13md + + A_copy = np.asfortranarray(A.copy()) + ITYPE_copy = np.asfortranarray(ITYPE.astype(np.int32)) + NBLOCK_copy = np.asfortranarray(NBLOCK.astype(np.int32)) + + bound, D, G, x, info = _ab13md(A_copy, ITYPE_copy, NBLOCK_copy) + + _check_info(info, 'ab13md') + + return bound, D, G + + +def tb01pd(n, m, p, A, B, C, job='M', equil='S', tol=0.0, ldwork=None): + """Minimal realization (slycot-compatible wrapper). + + slycot API: Ar, Br, Cr, nr = tb01pd(n, m, p, A, B, C, job, equil, tol) + slicot API: a, b, c, nr, nblk, info = tb01pd(job, equil, a, b, c, tol) + + Note: slicot tb01pd infers dimensions from array shapes. + + Returns + ------- + Ar : ndarray + Reduced A matrix. + Br : ndarray + Reduced B matrix. + Cr : ndarray + Reduced C matrix. + nr : int + Order of minimal realization. + """ + from slicot import tb01pd as _tb01pd + + # Extract actual-sized arrays (caller may pass pre-padded arrays) + A_copy = np.asfortranarray(A[:n, :n].copy()) + B_copy = np.asfortranarray(B[:n, :m].copy()) + C_copy = np.asfortranarray(C[:p, :n].copy()) + + if tol is None: + tol = 0.0 + + Ar_full, Br_full, Cr_full, nr, nblk, info = _tb01pd( + job, equil, A_copy, B_copy, C_copy, tol + ) + + _check_info(info, 'tb01pd') + + Ar = Ar_full[:nr, :nr].copy() + Br = Br_full[:nr, :].copy() + Cr = Cr_full[:, :nr].copy() + + return Ar, Br, Cr, nr + + +def _tb04ad_n1_fallback(m, p, A, B, C, D): + """Fallback for tb04ad when n=1 (scalar state). + + For n=1: T(s) = C * B / (s - a) + D where a = A[0,0] + Each output has the same denominator (s - a). + """ + a = A[0, 0] + + # Denominator: s - a = [1, -a] (high to low coefficients) + # All outputs share this denominator + index = np.ones(p, dtype=np.int32) # degree 1 for each output + dcoeff = np.zeros((p, 2), dtype=float, order='F') + for i in range(p): + dcoeff[i, :] = [1.0, -a] + + # Numerator for T[i,j]: C[i,:] @ B[:,j] + D[i,j] * (s - a) + # = D[i,j]*s + (C[i,:] @ B[:,j] - D[i,j]*a) + # = [D[i,j], C[i,:] @ B[:,j] - D[i,j]*a] + CB = C @ B + ucoeff = np.zeros((p, m, 2), dtype=float, order='F') + for i in range(p): + for j in range(m): + ucoeff[i, j, 0] = D[i, j] + ucoeff[i, j, 1] = CB[i, j] - D[i, j] * a + + # Return controllable realization (same as input for n=1) + return A.copy(), B.copy(), C.copy(), 1, index, dcoeff, ucoeff + + +def tb04ad(n, m, p, A, B, C, D, tol1=0.0, tol2=0.0, ldwork=None): + """State-space to transfer function (slycot-compatible wrapper). + + slycot API: A_ctrb, B_ctrb, C_ctrb, nctrb, index, dcoeff, ucoeff = + tb04ad(n, m, p, A, B, C, D, tol1) + slicot API: a_out, b_out, c_out, d_out, nr, index, dcoeff, ucoeff, info = + tb04ad(rowcol, a, b, c, d, tol1, tol2) + + Returns + ------- + A_ctrb : ndarray + Transformed A matrix (controllable realization). + B_ctrb : ndarray + Transformed B matrix. + C_ctrb : ndarray + Transformed C matrix. + nctrb : int + Order of controllable part. + index : ndarray + Degrees of the denominator polynomials per output. + dcoeff : ndarray + Denominator polynomial coefficients. + ucoeff : ndarray + Numerator polynomial coefficients. + """ + from slicot import tb04ad as _tb04ad + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + D_copy = np.asfortranarray(D.copy()) + + # slicot's tb04ad has a bug when n=1 and m >= 3 (returns info=-24) + # Use fallback for this case + if n == 1 and m >= 3: + return _tb04ad_n1_fallback(m, p, A_copy, B_copy, C_copy, D_copy) + + a_out, b_out, c_out, d_out, nr, index, dcoeff, ucoeff, info = _tb04ad( + 'R', A_copy, B_copy, C_copy, D_copy, tol1, tol2 + ) + + _check_info(info, 'tb04ad') + + return a_out, b_out, c_out, nr, index, dcoeff, ucoeff + + +def tb05ad(n, m, p, jomega, A, B, C, job='NG', ldwork=None): + """Frequency response evaluation (slycot-compatible wrapper). + + slycot API: (depends on job) + job='NG': at, bt, ct, g, hinvb + job='NH': g, hinvb + slicot API: g, rcond, a_hess, b_trans, c_trans, info = + tb05ad(baleig, inita, A, B, C, freq) + + Returns + ------- + Depends on job parameter. + """ + from slicot import tb05ad as _tb05ad + + A_copy = np.asfortranarray(A.copy()) + B_copy = np.asfortranarray(B.copy()) + C_copy = np.asfortranarray(C.copy()) + + # Map slycot job parameter to slicot parameters: + # job='NG' -> inita='G' (general A, compute Hessenberg) + # job='NH' -> inita='H' (A already in Hessenberg form) + baleig = 'N' + inita = 'G' if job == 'NG' else 'H' + + g, rcond, a_hess, b_trans, c_trans, info = _tb05ad( + baleig, inita, A_copy, B_copy, C_copy, jomega + ) + + _check_info(info, 'tb05ad') + + # hinvb = inv(jomega*I - A) * B is not returned by slicot + # but it's not actually used by callers, so return None + hinvb = None + + if job == 'NG': + # Return input arrays as "transformed" matrices for compatibility + # slicot's tb05ad doesn't return properly shaped transformed matrices + return A_copy, B_copy, C_copy, g, hinvb, info + else: + return g, hinvb, info + + +def _convert_col_to_row_common_den(m, p, index_c, dcoeff_c, ucoeff_c): + """Convert column-based common denominators to row-based. + + For 'C' mode: each column (input) has its own common denominator. + For 'R' mode: each row (output) has its own common denominator. + + This function computes row-based data from column-based data by finding + the product of all column denominators for each row (conservative LCM). + """ + from numpy.polynomial import polynomial as P + + # For each output row, compute product of all input column denominators + # (This is a conservative approximation of LCM that always works) + # Coefficients in numpy.polynomial format: [const, x, x^2, ...] + row_dens = [] + + for i in range(p): + lcm_poly = np.array([1.0]) + for j in range(m): + deg_j = index_c[j] + den_j = dcoeff_c[j, :deg_j + 1] + den_j_rev = den_j[::-1] + lcm_poly = P.polymul(lcm_poly, den_j_rev) + row_dens.append(lcm_poly) + + # First pass: compute all adjusted numerators and find max degree + adjusted_nums = [] + max_num_deg = 0 + + for i in range(p): + row_adjusted = [] + row_den = row_dens[i] + for j in range(m): + deg_j = index_c[j] + col_den_j = dcoeff_c[j, :deg_j + 1] + col_den_j_rev = col_den_j[::-1] + quot, _ = P.polydiv(row_den, col_den_j_rev) + + # Numerator has same degree as column denominator (right-padded format) + num_ij = ucoeff_c[i, j, :deg_j + 1] + if np.abs(num_ij).max() < 1e-15: + row_adjusted.append(None) + continue + num_ij_rev = num_ij[::-1] # low to high for numpy.polynomial + new_num = P.polymul(num_ij_rev, quot) + row_adjusted.append(new_num) + max_num_deg = max(max_num_deg, len(new_num) - 1) + adjusted_nums.append(row_adjusted) + + # Compute kdcoef_r as max of denominator and numerator degrees + max_den_deg = max(len(rd) - 1 for rd in row_dens) + kdcoef_r = max(max_den_deg, max_num_deg) + 1 + + # Build R-mode arrays + index_r = np.zeros(p, dtype=np.int32) + dcoeff_r = np.zeros((p, kdcoef_r), dtype=float, order='F') + ucoeff_r = np.zeros((p, m, kdcoef_r), dtype=float, order='F') + + for i in range(p): + row_den = row_dens[i] + deg_i = len(row_den) - 1 + index_r[i] = deg_i + dcoeff_r[i, :deg_i + 1] = row_den[::-1] + + for j in range(m): + new_num = adjusted_nums[i][j] + if new_num is None: + continue + new_num_rev = new_num[::-1] # high to low + deg_new = len(new_num_rev) - 1 # actual polynomial degree + # Right-align: place coefficients to match denominator indexing + # For proper TF, deg_new <= deg_i, so pad with leading zeros + start_idx = deg_i - deg_new + ucoeff_r[i, j, start_idx:deg_i + 1] = new_num_rev + + return index_r, dcoeff_r, ucoeff_r + + +def td04ad(rowcol, m, p, index, dcoeff, ucoeff, tol=0.0, ldwork=None): + """Transfer function to state-space (slycot-compatible wrapper). + + slycot API: nr, A, B, C, D = td04ad(rowcol, m, p, index, dcoeff, ucoeff, tol) + slicot API: nr, A, B, C, D, info = td04ad(rowcol, m, p, index, dcoeff, ucoeff, tol) + + Parameters + ---------- + rowcol : str + 'R' for rows over common denominators, 'C' for columns. + m : int + Number of system inputs. + p : int + Number of system outputs. + index : ndarray + Degrees of denominators (length m for 'C', length p for 'R'). + dcoeff : ndarray + Denominator coefficients (m x kdcoef for 'C', p x kdcoef for 'R'). + ucoeff : ndarray + Numerator coefficients (p x m x kdcoef). + + Returns + ------- + nr : int + Order of the resulting state-space system. + A, B, C, D : ndarray + State-space matrices. + """ + from slicot import td04ad as _td04ad + + # ucoeff may be padded to square; trim to (p, m, kdcoef) + ucoeff_trimmed = ucoeff[:p, :m, :] + + # slicot's td04ad has issues with rowcol='C' when p != m (non-square) + # Work around by converting to 'R' mode + if rowcol == 'C' and p != m: + index_r, dcoeff_r, ucoeff_r = _convert_col_to_row_common_den( + m, p, index, dcoeff, ucoeff_trimmed + ) + index_copy = np.asfortranarray(index_r, dtype=np.int32) + dcoeff_copy = np.asfortranarray(dcoeff_r) + ucoeff_copy = np.asfortranarray(ucoeff_r) + rowcol_actual = 'R' + else: + index_copy = np.asfortranarray(index.copy(), dtype=np.int32) + dcoeff_copy = np.asfortranarray(dcoeff.copy()) + ucoeff_copy = np.asfortranarray(ucoeff_trimmed.copy()) + rowcol_actual = rowcol + + nr, A, B, C, D, info = _td04ad( + rowcol_actual, m, p, index_copy, dcoeff_copy, ucoeff_copy, tol + ) + + _check_info(info, 'td04ad') + + return nr, A, B, C, D + + +def mb03rd(n, A, X, pmax=1.0, tol=0.0, ldwork=None): + """Block diagonal Schur form (slycot-compatible wrapper). + + slycot API: Aout, Xout, blsize, w = mb03rd(n, A, X, pmax) + slicot API: a, x, nblcks, blsize, wr, wi, info = + mb03rd(jobx, sort, a, pmax, x, tol) + + Returns + ------- + Aout : ndarray + Block diagonal Schur form. + Xout : ndarray + Transformation matrix. + blsize : ndarray + Block sizes. + w : ndarray + Eigenvalues (complex). + """ + from slicot import mb03rd as _mb03rd + + A_copy = np.asfortranarray(A.copy()) + X_copy = np.asfortranarray(X.copy()) + + Aout, Xout, nblcks, blsize, wr, wi, info = _mb03rd( + 'U', 'N', A_copy, pmax, X_copy, tol + ) + + _check_info(info, 'mb03rd') + + w = wr + 1j * wi + + return Aout, Xout, blsize[:nblcks], w + + +# If using slycot (not slicot), overwrite with direct imports from slycot +if _use_slycot and not _use_slicot: + from slycot import ( # noqa: F811 + sb03od, sb04md, sb04qd, sg03ad, + sb02md, sb02mt, sg02ad, sb01bd, + sb10ad, sb10hd, ab08nd, + ab09ad, ab09md, ab09nd, + ab13bd, ab13dd, ab13md, + tb01pd, tb04ad, tb05ad, td04ad, mb03rd, + ) + from slycot.exceptions import ( # noqa: F811 + SlycotResultWarning as SlicotResultWarning, + SlycotArithmeticError as SlicotArithmeticError, + ) + + from slycot import sb03md57 + + def sb03md(n, C, A, U, dico, job='X', fact='N', trana='N', ldwork=None): # noqa: F811 + """Wrapper for slycot's sb03md57.""" + ret = sb03md57(A, U, C, dico, job, fact, trana, ldwork) + return ret[2:] diff --git a/control/statefbk.py b/control/statefbk.py index b6e9c9655..40d9522f7 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -12,29 +12,21 @@ from . import statesp from .config import _process_legacy_keyword -from .exception import ControlArgument, ControlSlycot +from .exception import ControlArgument, ControlSlicot from .iosys import _process_indices, _process_labels, isctime, isdtime from .lti import LTI from .mateqn import care, dare from .nlsys import NonlinearIOSystem, interconnect from .statesp import StateSpace, _ssmatrix, ss -# Make sure we have access to the right Slycot routines +# Make sure we have access to the right slicot routines try: - from slycot import sb03md57 - - # wrap without the deprecation warning - def sb03md(n, C, A, U, dico, job='X',fact='N',trana='N',ldwork=None): - ret = sb03md57(A, U, C, dico, job, fact, trana, ldwork) - return ret[2:] + from .slicot_compat import sb03md except ImportError: - try: - from slycot import sb03md - except ImportError: - sb03md = None + sb03md = None try: - from slycot import sb03od + from .slicot_compat import sb03od except ImportError: sb03od = None @@ -159,11 +151,11 @@ def place_varga(A, B, p, dtime=False, alpha=None): """ - # Make sure that Slycot is installed + # Make sure that slicot is installed try: - from slycot import sb01bd + from .slicot_compat import sb01bd except ImportError: - raise ControlSlycot("can't find slycot module sb01bd") + raise ControlSlicot("can't find slicot module sb01bd") # Convert the system inputs to NumPy arrays A_mat = _ssmatrix(A, square=True, name="A") @@ -295,7 +287,7 @@ def lqr(*args, **kwargs): additional rows and columns in the `Q` matrix. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -442,7 +434,7 @@ def dlqr(*args, **kwargs): additional rows and columns in the `Q` matrix. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -1141,9 +1133,9 @@ def gram(sys, type): * if `type` is not 'c', 'o', 'cf' or 'of', or * if system is unstable (sys.A has eigenvalues not in left half plane). - ControlSlycot - If slycot routine sb03md cannot be found or - if slycot routine sb03od cannot be found. + ControlSlicot + If slicot routine sb03md cannot be found or + if slicot routine sb03od cannot be found. Examples -------- @@ -1181,7 +1173,7 @@ def gram(sys, type): # Compute Gramian by the Slycot routine sb03md # make sure Slycot is installed if sb03md is None: - raise ControlSlycot("can't find slycot module sb03md") + raise ControlSlicot("can't find slicot module sb03md") if type == 'c': tra = 'T' C = -sys.B @ sys.B.T @@ -1190,7 +1182,7 @@ def gram(sys, type): C = -sys.C.T @ sys.C n = sys.nstates U = np.zeros((n, n)) - A = np.array(sys.A) # convert to NumPy array for slycot + A = np.array(sys.A) # convert to NumPy array for slicot X, scale, sep, ferr, w = sb03md( n, C, A, U, dico, job='X', fact='N', trana=tra) gram = X @@ -1199,11 +1191,11 @@ def gram(sys, type): elif type == 'cf' or type == 'of': # Compute Cholesky factored Gramian from Slycot routine sb03od if sb03od is None: - raise ControlSlycot("can't find slycot module sb03od") + raise ControlSlicot("can't find slicot module sb03od") tra = 'N' n = sys.nstates Q = np.zeros((n, n)) - A = np.array(sys.A) # convert to NumPy array for slycot + A = np.array(sys.A) # convert to NumPy array for slicot if type == 'cf': m = sys.B.shape[1] B = np.zeros_like(A) diff --git a/control/statesp.py b/control/statesp.py index 65529b99d..83dde3194 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -32,7 +32,7 @@ from . import bdalg, config from .exception import ControlDimension, ControlMIMONotImplemented, \ - ControlSlycot, slycot_check + ControlSlicot, slicot_check from .frdata import FrequencyResponseData from .iosys import InputOutputSystem, NamedSignal, _process_iosys_keywords, \ _process_signal_list, _process_subsys_index, common_timebase, issiso @@ -41,7 +41,7 @@ from .nlsys import InterconnectedSystem, NonlinearIOSystem try: - from slycot import ab13dd + from .slicot_compat import ab13dd except ImportError: ab13dd = None @@ -789,7 +789,7 @@ def __call__(self, x, squeeze=None, warn_infinite=True): out = self.horner(x, warn_infinite=warn_infinite) return _process_frequency_response(self, x, out, squeeze=squeeze) - def slycot_laub(self, x): + def slicot_laub(self, x): """Laub's method to evaluate response at complex frequency. Evaluate transfer function at complex frequency using Laub's @@ -808,7 +808,7 @@ def slycot_laub(self, x): Frequency response. """ - from slycot import tb05ad + from .slicot_compat import tb05ad # Make sure the argument is a 1D array of complex numbers x_arr = np.atleast_1d(x).astype(complex, copy=False) @@ -889,7 +889,7 @@ def horner(self, x, warn_infinite=True): return out try: - out = self.slycot_laub(x_arr) + out = self.slicot_laub(x_arr) except (ImportError, Exception): # Fall back because either Slycot unavailable or cannot handle # certain cases. @@ -952,7 +952,7 @@ def zeros(self): # Use AB08ND from Slycot if it's available, otherwise use # scipy.lingalg.eigvals(). try: - from slycot import ab08nd + from .slicot_compat import ab08nd out = ab08nd(self.A.shape[0], self.B.shape[1], self.C.shape[0], self.A, self.B, self.C, self.D) @@ -1175,17 +1175,17 @@ def minreal(self, tol=0.0): """ if self.nstates: try: - from slycot import tb01pd - B = empty((self.nstates, max(self.ninputs, self.noutputs))) + from .slicot_compat import tb01pd + B = zeros((self.nstates, max(self.ninputs, self.noutputs))) B[:, :self.ninputs] = self.B - C = empty((max(self.noutputs, self.ninputs), self.nstates)) + C = zeros((max(self.noutputs, self.ninputs), self.nstates)) C[:self.noutputs, :] = self.C A, B, C, nr = tb01pd(self.nstates, self.ninputs, self.noutputs, self.A, B, C, tol=tol) return StateSpace(A[:nr, :nr], B[:nr, :self.ninputs], C[:self.noutputs, :nr], self.D, self.dt) except ImportError: - raise TypeError("minreal requires slycot tb01pd") + raise TypeError("minreal requires slicot tb01pd") else: return StateSpace(self) @@ -1682,8 +1682,8 @@ def ss(*args, **kwargs): `config.defaults['statesp.remove_useless_states']` (default = False). method : str, optional Set the method used for converting a transfer function to a state - space system. Current methods are 'slycot' and 'scipy'. If set to - None (default), try 'slycot' first and then 'scipy' (SISO only). + space system. Current methods are 'slicot' and 'scipy'. If set to + None (default), try 'slicot' first and then 'scipy' (SISO only). Returns ------- @@ -1910,7 +1910,7 @@ def tf2ss(*args, **kwargs): with a unique integer id. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy' (SISO only). Raises @@ -1928,7 +1928,7 @@ def tf2ss(*args, **kwargs): Notes ----- - The `slycot` routine used to convert a transfer function into state space + The `slicot` routine used to convert a transfer function into state space form appears to have a bug and in some (rare) instances may not return a system with the same poles as the input transfer function. For SISO systems, setting `method` = 'scipy' can be used as an alternative. @@ -1996,7 +1996,7 @@ def linfnorm(sys, tol=1e-10): See Also -------- - slycot.ab13dd + slicot.ab13dd Notes ----- @@ -2007,7 +2007,7 @@ def linfnorm(sys, tol=1e-10): """ if ab13dd is None: - raise ControlSlycot("Can't find slycot module ab13dd") + raise ControlSlicot("Can't find slicot module ab13dd") a, b, c, d = ssdata(_convert_to_statespace(sys)) e = np.eye(a.shape[0]) @@ -2394,11 +2394,11 @@ def _convert_to_statespace(sys, use_prefix_suffix=False, method=None): raise ValueError("transfer function is non-proper; can't " "convert to StateSpace system") - if method is None and slycot_check() or method == 'slycot': - if not slycot_check(): - raise ValueError("method='slycot' requires slycot") + if method is None and slicot_check() or method == 'slicot': + if not slicot_check(): + raise ValueError("method='slicot' requires slicot") - from slycot import td04ad + from .slicot_compat import td04ad # Change the numerator and denominator arrays so that the transfer # function matrix has a common denominator. diff --git a/control/stochsys.py b/control/stochsys.py index 756d83e13..b66b43a67 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -81,7 +81,7 @@ def lqe(*args, **kwargs): Cross covariance matrix. Not currently implemented. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -218,7 +218,7 @@ def dlqe(*args, **kwargs): Cross covariance matrix (not yet supported). method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns diff --git a/control/sysnorm.py b/control/sysnorm.py index fecdd7095..260aa9f95 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -16,7 +16,7 @@ #------------------------------------------------------------------------------ -def _h2norm_slycot(sys, print_warning=True): +def _h2norm_slicot(sys, print_warning=True): """H2 norm of a linear system. For internal use. Requires Slycot. See Also @@ -24,17 +24,10 @@ def _h2norm_slycot(sys, print_warning=True): slycot.ab13bd """ - # See: https://github.com/python-control/Slycot/issues/199 try: - from slycot import ab13bd + from .slicot_compat import ab13bd, SlicotArithmeticError except ImportError: - ct.ControlSlycot("Can't find slycot module ab13bd") - - try: - from slycot.exceptions import SlycotArithmeticError - except ImportError: - raise ct.ControlSlycot( - "Can't find slycot class SlycotArithmeticError") + raise ct.ControlSlicot("Can't find slicot module ab13bd") A, B, C, D = ct.ssdata(ct.ss(sys)) @@ -60,7 +53,7 @@ def _h2norm_slycot(sys, print_warning=True): try: norm = ab13bd(dico, jobn, n, m, p, A, B, C, D) - except SlycotArithmeticError as e: + except SlicotArithmeticError as e: if e.info == 3: if print_warning: warnings.warn( @@ -100,7 +93,7 @@ def system_norm(system, p=2, tol=1e-6, print_warning=True, method=None): Print warning message in case norm value may be uncertain. method : str, optional Set the method used for computing the result. Current methods are - 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + 'slicot' and 'scipy'. If set to None (default), try 'slicot' first and then 'scipy'. Returns @@ -133,7 +126,7 @@ def system_norm(system, p=2, tol=1e-6, print_warning=True, method=None): D = G.D # Decide what method to use - method = ct.mateqn._slycot_or_scipy(method) + method = ct.mateqn._slicot_or_scipy(method) # ------------------- # H2 norm computation @@ -164,8 +157,8 @@ def system_norm(system, p=2, tol=1e-6, print_warning=True, method=None): else: # Use slycot, if available, to compute (finite) norm - if method == 'slycot': - return _h2norm_slycot(G, print_warning) + if method == 'slicot': + return _h2norm_slicot(G, print_warning) # Else use scipy else: @@ -210,8 +203,8 @@ def system_norm(system, p=2, tol=1e-6, print_warning=True, method=None): return float('inf') else: # Use slycot, if available, to compute (finite) norm - if method == 'slycot': - return _h2norm_slycot(G, print_warning) + if method == 'slicot': + return _h2norm_slicot(G, print_warning) # Else use scipy else: @@ -259,7 +252,7 @@ def system_norm(system, p=2, tol=1e-6, print_warning=True, method=None): return float('inf') # Use slycot, if available, to compute (finite) norm - if method == 'slycot': + if method == 'slicot': return ct.linfnorm(G, tol)[0] # Else use scipy diff --git a/control/tests/canonical_test.py b/control/tests/canonical_test.py index 63afd51c3..8eaf01962 100644 --- a/control/tests/canonical_test.py +++ b/control/tests/canonical_test.py @@ -242,7 +242,7 @@ def block_diag_from_eig(eigvals): return scipy.linalg.block_diag(*blocks) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.parametrize( "eigvals, condmax, blksizes", [ @@ -267,7 +267,7 @@ def test_bdschur_ref(eigvals, condmax, blksizes): np.testing.assert_array_almost_equal(solve(t, a) @ t, b) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.parametrize( "eigvals, sorted_blk_eigvals, sort", [ @@ -298,7 +298,7 @@ def test_bdschur_sort(eigvals, sorted_blk_eigvals, sort): blk_eigval.imag) -@pytest.mark.slycot +@pytest.mark.slicot def test_bdschur_defective(): # the eigenvalues of this simple defective matrix cannot be separated # a previous version of the bdschur would fail on this @@ -321,14 +321,14 @@ def test_bdschur_condmax_lt_1(): bdschur(1, condmax=np.nextafter(1, 0)) -@pytest.mark.slycot +@pytest.mark.slicot def test_bdschur_invalid_sort(): # sort must be in ('continuous', 'discrete') with pytest.raises(ValueError): bdschur(1, sort='no-such-sort') -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.parametrize( "A_true, B_true, C_true, D_true", [(np.diag([4.0, 3.0, 2.0, 1.0]), # order from largest to smallest @@ -388,7 +388,7 @@ def test_modal_form(A_true, B_true, C_true, D_true): C @ np.linalg.matrix_power(A, i) @ B) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.parametrize( "condmax, len_blksizes", [(1.1, 1), @@ -407,7 +407,7 @@ def test_modal_form_condmax(condmax, len_blksizes): np.testing.assert_array_almost_equal(zsys.D, xsys.D) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.parametrize( "sys_type", ['continuous', diff --git a/control/tests/conftest.py b/control/tests/conftest.py index d055690d1..f110334af 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -7,14 +7,14 @@ import control def pytest_runtest_setup(item): - if not control.exception.slycot_check(): - if any(mark.name == 'slycot' + if not control.exception.slicot_check(): + if any(mark.name == 'slicot' for mark in item.iter_markers()): - pytest.skip("slycot not installed") - elif any(mark.name == 'noslycot' + pytest.skip("slicot not installed") + elif any(mark.name == 'noslicot' for mark in item.iter_markers()): - # used, e.g., for tests checking ControlSlycot - pytest.skip("slycot installed") + # used, e.g., for tests checking ControlSlicot + pytest.skip("slicot installed") if (not control.exception.cvxopt_check() and any(mark.name == 'cvxopt' diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 9cdabbe6c..4f8da92f8 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -46,12 +46,12 @@ def printSys(self, sys, ind): @pytest.mark.usefixtures("legacy_plot_signature") @pytest.mark.parametrize("states", range(1, maxStates)) - # If slycot is not installed, just check SISO + # If slicot is not installed, just check SISO @pytest.mark.parametrize("inputs", - [1] + [pytest.param(i, marks=pytest.mark.slycot) + [1] + [pytest.param(i, marks=pytest.mark.slicot) for i in range(2, 5)]) @pytest.mark.parametrize("outputs", - [1] + [pytest.param(i, marks=pytest.mark.slycot) + [1] + [pytest.param(i, marks=pytest.mark.slicot) for i in range(2, 5)]) def testConvert(self, fixedseed, states, inputs, outputs): """Test state space to transfer function conversion. @@ -150,14 +150,14 @@ def testConvert(self, fixedseed, states, inputs, outputs): ssorig_imag, tfxfrm_imag, decimal=5) - @pytest.mark.parametrize('have_slycot', - [pytest.param(True, marks=pytest.mark.slycot), - pytest.param(False, marks=pytest.mark.noslycot)]) - def testConvertMIMO(self, have_slycot): + @pytest.mark.parametrize('have_slicot', + [pytest.param(True, marks=pytest.mark.slicot), + pytest.param(False, marks=pytest.mark.noslicot)]) + def testConvertMIMO(self, have_slicot): """Test state space to transfer function conversion. Do a MIMO conversion and make sure that it is processed - correctly both with and without slycot + correctly both with and without slicot Example from issue gh-120, jgoppert """ @@ -171,7 +171,7 @@ def testConvertMIMO(self, have_slycot): [0.008, 1.39, 48.78]]]) # Convert to state space and look for an error - if not have_slycot: + if not have_slicot: with pytest.raises(ControlMIMONotImplemented): tf2ss(tsys) else: @@ -219,7 +219,7 @@ def testSs2tfStaticMimo(self): np.testing.assert_allclose(numref, np.array(gtf.num) / np.array(gtf.den)) - @pytest.mark.slycot + @pytest.mark.slicot def testTf2SsDuplicatePoles(self): """Tests for 'too few poles for MIMO tf gh-111'""" num = [[[1], [0]], @@ -230,7 +230,7 @@ def testTf2SsDuplicatePoles(self): s = ss(g) np.testing.assert_allclose(g.poles(), s.poles()) - @pytest.mark.slycot + @pytest.mark.slicot def test_tf2ss_robustness(self): """Unit test to make sure that tf2ss is working correctly. gh-240""" num = [ [[0], [1]], [[1], [0]] ] diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index ab8ce3be6..da703054a 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -565,7 +565,7 @@ def test_mul_mimo_siso(self, left, right, expected): np.testing.assert_array_almost_equal(expected_frd.omega, result.omega) np.testing.assert_array_almost_equal(expected_frd.frdata, result.frdata) - @pytest.mark.slycot + @pytest.mark.slicot def test_truediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) @@ -590,7 +590,7 @@ def test_truediv_mimo_siso(self): np.testing.assert_array_almost_equal(expected.omega, result.omega) np.testing.assert_array_almost_equal(expected.frdata, result.frdata) - @pytest.mark.slycot + @pytest.mark.slicot def test_rtruediv_mimo_siso(self): omega = np.logspace(-1, 1, 10) tf_mimo = TransferFunction([1], [1, 0]) * np.eye(2) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 5112a99e9..c03f4bd3f 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -60,7 +60,7 @@ def test_freqresp_siso(ss_siso): @pytest.mark.filterwarnings(r"ignore:freqresp\(\) is deprecated") -@pytest.mark.slycot +@pytest.mark.slicot def test_freqresp_mimo_legacy(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) @@ -69,7 +69,7 @@ def test_freqresp_mimo_legacy(ss_mimo): ctrl.freqresp(tf_mimo, omega) -@pytest.mark.slycot +@pytest.mark.slicot def test_freqresp_mimo(ss_mimo): """Test MIMO frequency response calls""" omega = np.linspace(10e-2, 10e2, 1000) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index ccce76f34..06adb484b 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -57,7 +57,7 @@ def test_summation_exceptions(): @pytest.mark.parametrize("dim", - [1, pytest.param(3, marks=pytest.mark.slycot)]) + [1, pytest.param(3, marks=pytest.mark.slicot)]) def test_interconnect_implicit(dim): """Test the use of implicit connections in interconnect()""" import random diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index dd95f3505..f4e02b65b 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -57,7 +57,7 @@ def test_issiso(self): assert issiso(sys) assert issiso(sys, strict=True) - @pytest.mark.slycot + @pytest.mark.slicot def test_issiso_mimo(self): # MIMO transfer function sys = tf([[[-1, 41], [1]], [[1, 2], [3, 4]]], @@ -190,7 +190,7 @@ def test_isdtime(self, objfun, arg, dt, ref, strictref): def p(*args): # convenience for parametrize below - return pytest.param(*args, marks=pytest.mark.slycot) + return pytest.param(*args, marks=pytest.mark.slicot) @pytest.mark.usefixtures("editsdefaults") @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd]) diff --git a/control/tests/margin_test.py b/control/tests/margin_test.py index c8be4ee6c..6e5f9a331 100644 --- a/control/tests/margin_test.py +++ b/control/tests/margin_test.py @@ -393,7 +393,7 @@ def test_siso_disk_margin(): DM = disk_margins(L, omega, skew=1.0)[0] assert_allclose([DM], [SM], atol=0.01) -@pytest.mark.slycot +@pytest.mark.slicot def test_mimo_disk_margin(): # Frequencies of interest omega = np.logspace(-1, 3, 1001) @@ -417,9 +417,9 @@ def test_mimo_disk_margin(): assert_allclose([DPMi], [21.26], atol=0.1) # disk-based phase margin of 21.26 deg -@pytest.mark.noslycot +@pytest.mark.noslicot def test_mimo_disk_margin_exception(): - # Slycot not installed. Should throw exception. + # Slicot not installed. Should throw exception. # Frequencies of interest omega = np.logspace(-1, 3, 1001) @@ -428,7 +428,7 @@ def test_mimo_disk_margin_exception(): K = ss([], [], [], [[1, -2], [0, 1]]) # controller Lo = P * K # loop transfer function, broken at plant output with pytest.raises(ControlMIMONotImplemented,\ - match="Need slycot to compute MIMO disk_margins"): + match="Need slicot to compute MIMO disk_margins"): DMo, DGMo, DPMo = disk_margins(Lo, omega, skew=0.0) def test_siso_disk_margin_return_all(): @@ -449,7 +449,7 @@ def test_siso_disk_margin_return_all(): atol=0.1) # disk-based phase margin of 25.8 deg -@pytest.mark.slycot +@pytest.mark.slicot def test_mimo_disk_margin_return_all(): # Frequencies of interest omega = np.logspace(-1, 3, 1001) diff --git a/control/tests/mateqn_test.py b/control/tests/mateqn_test.py index 77bf553bf..e8befcd7c 100644 --- a/control/tests/mateqn_test.py +++ b/control/tests/mateqn_test.py @@ -48,7 +48,7 @@ class TestMatrixEquations: @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_lyap(self, method): A = array([[-1, 1], [-1, 0]]) Q = array([[1, 0], [0, 1]]) @@ -63,13 +63,13 @@ def test_lyap(self, method): assert_array_almost_equal(A @ X + X @ A.T + Q, zeros((2,2))) # Compare methods - if method == 'slycot': + if method == 'slicot': X_scipy = lyap(A, Q, method='scipy') assert_array_almost_equal(X_scipy, X) @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_lyap_sylvester(self, method): A = 5 B = array([[4, 3], [4, 3]]) @@ -86,11 +86,11 @@ def test_lyap_sylvester(self, method): assert_array_almost_equal(A @ X + X @ B + C, zeros((2,2))) # Compare methods - if method=='slycot': + if method=='slicot': X_scipy = lyap(A, B, C, method='scipy') assert_array_almost_equal(X_scipy, X) - @pytest.mark.slycot + @pytest.mark.slicot def test_lyap_g(self): A = array([[-1, 2], [-3, -4]]) Q = array([[3, 1], [1, 1]]) @@ -106,7 +106,7 @@ def test_lyap_g(self): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_dlyap(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[1,0],[0,1]]) @@ -121,11 +121,11 @@ def test_dlyap(self, method): assert_array_almost_equal(A @ X @ A.T - X + Q, zeros((2,2))) # Compare methods - if method=='slycot': + if method=='slicot': X_scipy = dlyap(A,Q, method='scipy') assert_array_almost_equal(X_scipy, X) - @pytest.mark.slycot + @pytest.mark.slicot def test_dlyap_g(self): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[3, 1],[1, 1]]) @@ -139,7 +139,7 @@ def test_dlyap_g(self): with pytest.raises(ControlArgument, match="'scipy' not valid"): X = dlyap(A, Q, None, E, method='scipy') - @pytest.mark.slycot + @pytest.mark.slicot def test_dlyap_sylvester(self): A = 5 B = array([[4, 3], [4, 3]]) @@ -161,7 +161,7 @@ def test_dlyap_sylvester(self): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_care(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) @@ -175,7 +175,7 @@ def test_care(self, method): assert_array_almost_equal(B.T @ X, G) # Compare methods - if method == 'slycot': + if method == 'slicot': X_scipy, L_scipy, G_scipy = care(A, B, Q, method='scipy') assert_array_almost_equal(X_scipy, X) assert_array_almost_equal(np.sort(L_scipy), np.sort(L)) @@ -183,7 +183,7 @@ def test_care(self, method): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_care_g(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) @@ -202,7 +202,7 @@ def test_care_g(self, method): zeros((2,2))) # Compare methods - if method=='slycot': + if method=='slicot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') assert_array_almost_equal(X_scipy, X) @@ -211,7 +211,7 @@ def test_care_g(self, method): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_care_g2(self, method): A = array([[-2, -1],[-1, -1]]) Q = array([[0, 0],[0, 1]]) @@ -230,7 +230,7 @@ def test_care_g2(self, method): assert_array_almost_equal(Gref , G) # Compare methods - if method=='slycot': + if method=='slicot': X_scipy, L_scipy, G_scipy = care( A, B, Q, R, S, E, method='scipy') assert_array_almost_equal(X_scipy, X) @@ -239,7 +239,7 @@ def test_care_g2(self, method): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_dare(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 0]]) @@ -274,7 +274,7 @@ def test_dare(self, method): lam = eigvals(A - B @ G) assert_array_less(abs(lam), 1.0) - @pytest.mark.slycot + @pytest.mark.slicot def test_dare_compare(self): A = np.array([[-0.6, 0], [-0.1, -0.4]]) Q = np.array([[2, 1], [1, 0]]) @@ -294,7 +294,7 @@ def test_dare_compare(self): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_dare_g(self, method): A = array([[-0.6, 0],[-0.1, -0.4]]) Q = array([[2, 1],[1, 3]]) @@ -314,7 +314,7 @@ def test_dare_g(self, method): lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) # Compare methods - if method=='slycot': + if method=='slicot': X_scipy, L_scipy, G_scipy = dare( A, B, Q, R, S, E, method='scipy') assert_array_almost_equal(X_scipy, X) @@ -323,7 +323,7 @@ def test_dare_g(self, method): @pytest.mark.parametrize('method', ['scipy', - pytest.param('slycot', marks=pytest.mark.slycot)]) + pytest.param('slicot', marks=pytest.mark.slicot)]) def test_dare_g2(self, method): A = array([[-0.6, 0], [-0.1, -0.4]]) Q = array([[2, 1], [1, 3]]) @@ -346,7 +346,7 @@ def test_dare_g2(self, method): lam = eigvals(A - B @ G, E) assert_array_less(abs(lam), 1.0) - if method=='slycot': + if method=='slicot': X_scipy, L_scipy, G_scipy = dare( A, B, Q, R, S, E, method='scipy') assert_array_almost_equal(X_scipy, X) diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index d1a71bce3..a647240f4 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -486,21 +486,21 @@ def testEvalfr_mimo(self, mimo): ref = np.array([[44.8 - 21.4j, 0.], [0., 44.8 - 21.4j]]) np.testing.assert_array_almost_equal(fr, ref) - @pytest.mark.slycot + @pytest.mark.slicot def testHsvd(self, siso): """Call hsvd()""" hsvd(siso.ss1) hsvd(siso.ss2) hsvd(siso.ss3) - @pytest.mark.slycot + @pytest.mark.slicot def testBalred(self, siso): """Call balred()""" balred(siso.ss1, 1) balred(siso.ss2, 2) balred(siso.ss3, [2, 2]) - @pytest.mark.slycot + @pytest.mark.slicot def testModred(self, siso): """Call modred()""" modred(siso.ss1, [1]) @@ -508,7 +508,7 @@ def testModred(self, siso): modred(siso.ss1, [1], 'matchdc') modred(siso.ss1, [1], 'truncate') - @pytest.mark.slycot + @pytest.mark.slicot def testPlace_varga(self, siso): """Call place_varga()""" place_varga(siso.ss1.A, siso.ss1.B, [-2, -2]) @@ -551,7 +551,7 @@ def testObsv(self, siso): obsv(siso.ss1.A, siso.ss1.C) obsv(siso.ss2.A, siso.ss2.C) - @pytest.mark.slycot + @pytest.mark.slicot def testGram(self, siso): """Call gram()""" gram(siso.ss1, 'c') @@ -695,7 +695,7 @@ def testFRD(self): frd2 = frd(frd1.frdata[0, 0, :], omega) assert isinstance(frd2, FRD) - @pytest.mark.slycot + @pytest.mark.slicot def testMinreal(self, verbose=False): """Test a minreal model reduction""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index e8223184c..800b884e8 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -13,12 +13,12 @@ from itertools import permutations -@pytest.fixture -def fixedseed(scope="class"): +@pytest.fixture(scope="class") +def fixedseed(): np.random.seed(5) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.usefixtures("fixedseed") class TestMinreal: """Tests for the StateSpace class.""" diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index c2773231b..df3f05873 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -19,7 +19,7 @@ class TestModelsimp: """Test model reduction functions""" - @pytest.mark.slycot + @pytest.mark.slicot def testHSVD(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -389,7 +389,7 @@ def testModredTruncate(self): np.testing.assert_array_almost_equal(rsys.D, Drtrue) - @pytest.mark.slycot + @pytest.mark.slicot def testBalredTruncate(self): # controlable canonical realization computed in matlab for the transfer # function: @@ -414,7 +414,7 @@ def testBalredTruncate(self): Crtrue = np.array([[0.9057, 0.4068]]) Drtrue = np.array([[0.]]) - # Look for possible changes in state in slycot + # Look for possible changes in state in slicot T1 = np.array([[1, 0], [0, -1]]) T2 = np.array([[-1, 0], [0, 1]]) T3 = np.array([[0, 1], [1, 0]]) @@ -430,7 +430,7 @@ def testBalredTruncate(self): np.testing.assert_array_almost_equal(Cr, Crtrue, decimal=4) np.testing.assert_array_almost_equal(Dr, Drtrue, decimal=4) - @pytest.mark.slycot + @pytest.mark.slicot def testBalredMatchDC(self): # controlable canonical realization computed in matlab for the transfer # function: @@ -457,7 +457,7 @@ def testBalredMatchDC(self): Crtrue = np.array([[1.36235673, 1.03114388]]) Drtrue = np.array([[-0.08383902]]) - # Look for possible changes in state in slycot + # Look for possible changes in state in slicot T1 = np.array([[1, 0], [0, -1]]) T2 = np.array([[-1, 0], [0, 1]]) T3 = np.array([[0, 1], [1, 0]]) diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 8c44f5980..3682ac34f 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -81,7 +81,7 @@ def test_named_ss(): def p(*args): # convenience for parametrize below - return pytest.param(*args, marks=pytest.mark.slycot) + return pytest.param(*args, marks=pytest.mark.slicot) @pytest.mark.parametrize("fun, args, kwargs", [ diff --git a/control/tests/optimal_test.py b/control/tests/optimal_test.py index fb3f4e716..b30a520f9 100644 --- a/control/tests/optimal_test.py +++ b/control/tests/optimal_test.py @@ -102,7 +102,7 @@ def test_finite_horizon_simple(method): # optimal control problem with terminal cost set to LQR "cost to go" # gives the same answer as LQR. # -@pytest.mark.slycot +@pytest.mark.slicot def test_discrete_lqr(): # oscillator model defined in 2D # Source: https://www.mpt3.org/UI/RegulationProblem diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index 8434ea6cd..660d0dc0c 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -9,7 +9,7 @@ class TestHinf: - @pytest.mark.slycot + @pytest.mark.slicot def testHinfsyn(self): """Test hinfsyn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -31,7 +31,7 @@ def testHinfsyn(self): class TestH2: - @pytest.mark.slycot + @pytest.mark.slicot def testH2syn(self): """Test h2syn""" p = ss(-1, [[1, 1]], [[1], [1]], [[0, 1], [1, 0]]) @@ -70,7 +70,7 @@ def siso_almost_equal(self, g, h): "sys 2:\n" "{}".format(maxnum, g, h)) - @pytest.mark.slycot + @pytest.mark.slicot def testSisoW1(self): """SISO plant with S weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -87,7 +87,7 @@ def testSisoW1(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @pytest.mark.slycot + @pytest.mark.slicot def testSisoW2(self): """SISO plant with KS weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -104,7 +104,7 @@ def testSisoW2(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @pytest.mark.slycot + @pytest.mark.slicot def testSisoW3(self): """SISO plant with T weighting""" g = ss([-1.], [1.], [1.], [1.]) @@ -121,7 +121,7 @@ def testSisoW3(self): # u->v should be -g self.siso_almost_equal(-g, p[1, 1]) - @pytest.mark.slycot + @pytest.mark.slicot def testSisoW123(self): """SISO plant with all weights""" g = ss([-1.], [1.], [1.], [1.]) @@ -148,7 +148,7 @@ def testSisoW123(self): # u->v should be -g self.siso_almost_equal(-g, p[3, 1]) - @pytest.mark.slycot + @pytest.mark.slicot def testMimoW1(self): """MIMO plant with S weighting""" g = ss([[-1., -2], [-3, -4]], @@ -180,7 +180,7 @@ def testMimoW1(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @pytest.mark.slycot + @pytest.mark.slicot def testMimoW2(self): """MIMO plant with KS weighting""" g = ss([[-1., -2], [-3, -4]], @@ -212,7 +212,7 @@ def testMimoW2(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @pytest.mark.slycot + @pytest.mark.slicot def testMimoW3(self): """MIMO plant with T weighting""" g = ss([[-1., -2], [-3, -4]], @@ -244,7 +244,7 @@ def testMimoW3(self): self.siso_almost_equal(-g[1, 0], p[3, 2]) self.siso_almost_equal(-g[1, 1], p[3, 3]) - @pytest.mark.slycot + @pytest.mark.slicot def testMimoW123(self): """MIMO plant with all weights""" g = ss([[-1., -2], [-3, -4]], @@ -306,7 +306,7 @@ def testMimoW123(self): self.siso_almost_equal(-g[1, 0], p[7, 2]) self.siso_almost_equal(-g[1, 1], p[7, 3]) - @pytest.mark.slycot + @pytest.mark.slicot def testErrors(self): """Error cases handled""" from control import augw, ss @@ -329,7 +329,7 @@ class TestMixsyn: """Test control.robust.mixsyn""" # it's a relatively simple wrapper; compare results with augw, hinfsyn - @pytest.mark.slycot + @pytest.mark.slicot def testSiso(self): """mixsyn with SISO system""" # Skogestad+Postlethwaite, Multivariable Feedback Control, 1st Ed., Example 2.11 diff --git a/control/tests/slycot_convert_test.py b/control/tests/slicot_convert_test.py similarity index 96% rename from control/tests/slycot_convert_test.py rename to control/tests/slicot_convert_test.py index 2739a4cf1..1774fdb56 100644 --- a/control/tests/slycot_convert_test.py +++ b/control/tests/slicot_convert_test.py @@ -1,6 +1,6 @@ -"""slycot_convert_test.py - test SLICOT-based conversions +"""slicot_convert_test.py - test SLICOT-based conversions -RMM, 30 Mar 2011 (based on TestSlycot from v0.4a) +RMM, 30 Mar 2011 (based on TestSlicot from v0.4a) """ import numpy as np @@ -20,12 +20,12 @@ def fixedseed(): np.random.seed(0) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.usefixtures("fixedseed") -class TestSlycot: - """Test Slycot system conversion +class TestSlicot: + """Test slicot system conversion - TestSlycot compares transfer function and state space conversions for + TestSlicot compares transfer function and state space conversions for various numbers of inputs,outputs and states. 1. Usually passes for SISO systems of any state dim, occasonally, there will be a dimension mismatch if the original randomly @@ -72,7 +72,7 @@ def testTF(self, states, outputs, inputs, testNum, verbose): comparison of transfer function coefficients. Similar to convert_test, but tests at a lower level. """ - from slycot import tb04ad, td04ad + from control.slicot_compat import tb04ad, td04ad ssOriginal = rss(states, outputs, inputs) if (verbose): @@ -135,7 +135,7 @@ def testFreqResp(self, states, outputs, inputs, testNum, verbose): original SS. They generally are different realizations but have same freq resp. Currently this test may only be applied to SISO systems. """ - from slycot import tb04ad, td04ad + from control.slicot_compat import tb04ad, td04ad ssOriginal = rss(states, outputs, inputs) diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 97cf7be68..58688e2cc 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -11,7 +11,7 @@ import control as ct from control import poles, rss, ss, tf -from control.exception import ControlDimension, ControlSlycot, \ +from control.exception import ControlDimension, ControlSlicot, \ ControlArgument from control.mateqn import care, dare from control.statefbk import (ctrb, obsv, place, place_varga, lqr, dlqr, @@ -127,7 +127,7 @@ def testCtrbObsvDuality(self): Wo = np.transpose(obsv(A, C)) np.testing.assert_array_almost_equal(Wc,Wo) - @pytest.mark.slycot + @pytest.mark.slicot def testGramWc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -143,7 +143,7 @@ def testGramWc(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @pytest.mark.slycot + @pytest.mark.slicot def testGramWc2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -160,7 +160,7 @@ def testGramWc2(self): Wc = gram(sysd, 'c') np.testing.assert_array_almost_equal(Wc, Wctrue) - @pytest.mark.slycot + @pytest.mark.slicot def testGramRc(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -176,7 +176,7 @@ def testGramRc(self): Rc = gram(sysd, 'cf') np.testing.assert_array_almost_equal(Rc, Rctrue) - @pytest.mark.slycot + @pytest.mark.slicot def testGramWo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -192,7 +192,7 @@ def testGramWo(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @pytest.mark.slycot + @pytest.mark.slicot def testGramWo2(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5.], [7.]]) @@ -208,7 +208,7 @@ def testGramWo2(self): Wo = gram(sysd, 'o') np.testing.assert_array_almost_equal(Wo, Wotrue) - @pytest.mark.slycot + @pytest.mark.slicot def testGramRo(self): A = np.array([[1., -2.], [3., -4.]]) B = np.array([[5., 6.], [7., 8.]]) @@ -317,7 +317,7 @@ def testPlace(self): with pytest.raises(ValueError): place(A, B, P_repeated) - @pytest.mark.slycot + @pytest.mark.slicot def testPlace_varga_continuous(self): """ Check that we can place eigenvalues for dtime=False @@ -344,7 +344,7 @@ def testPlace_varga_continuous(self): self.checkPlaced(P, P_placed) - @pytest.mark.slycot + @pytest.mark.slicot def testPlace_varga_continuous_partial_eigs(self): """ Check that we are able to use the alpha parameter to only place @@ -364,7 +364,7 @@ def testPlace_varga_continuous_partial_eigs(self): # No guarantee of the ordering, so sort them self.checkPlaced(P_expected, P_placed) - @pytest.mark.slycot + @pytest.mark.slicot def testPlace_varga_discrete(self): """ Check that we can place poles using dtime=True (discrete time) @@ -378,7 +378,7 @@ def testPlace_varga_discrete(self): # No guarantee of the ordering, so sort them self.checkPlaced(P, P_placed) - @pytest.mark.slycot + @pytest.mark.slicot def testPlace_varga_discrete_partial_eigs(self): """" Check that we can only assign a single eigenvalue in the discrete @@ -413,7 +413,7 @@ def check_DLQR(self, K, S, poles, Q, R): @pytest.mark.parametrize("method", [None, - pytest.param('slycot', marks=pytest.mark.slycot), + pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) def test_LQR_integrator(self, method): A, B, Q, R = (np.array([[X]]) for X in [0., 1., 10., 2.]) @@ -422,7 +422,7 @@ def test_LQR_integrator(self, method): @pytest.mark.parametrize("method", [None, - pytest.param('slycot', marks=pytest.mark.slycot), + pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) def test_LQR_3args(self, method): sys = ss(0., 1., 1., 0.) @@ -432,7 +432,7 @@ def test_LQR_3args(self, method): @pytest.mark.parametrize("method", [None, - pytest.param('slycot', marks=pytest.mark.slycot), + pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) def test_DLQR_3args(self, method): dsys = ss(0., 1., 1., 0., .1) @@ -451,12 +451,12 @@ def test_lqr_badmethod(self, cdlqr): with pytest.raises(ControlArgument, match="Unknown method"): K, S, poles = cdlqr(A, B, Q, R, method='nosuchmethod') - @pytest.mark.noslycot + @pytest.mark.noslicot @pytest.mark.parametrize("cdlqr", [lqr, dlqr]) - def test_lqr_slycot_not_installed(self, cdlqr): + def test_lqr_slicot_not_installed(self, cdlqr): A, B, Q, R = 0, 1, 10, 2 - with pytest.raises(ControlSlycot, match="Can't find slycot"): - K, S, poles = cdlqr(A, B, Q, R, method='slycot') + with pytest.raises(ControlSlicot, match="Can't find slicot"): + K, S, poles = cdlqr(A, B, Q, R, method='slicot') @pytest.mark.xfail(reason="warning not implemented") def testLQR_warning(self): @@ -515,7 +515,7 @@ def test_lqr_call_format(self, cdlqr): with pytest.raises(ct.ControlArgument, match="not enough input"): K, S, E = cdlqr(sys.A, sys.B) - # First argument is the wrong type (use SISO for non-slycot tests) + # First argument is the wrong type (use SISO for non-slicot tests) sys_tf = tf(rss(3, 1, 1)) sys_tf.dt = None # treat as either continuous or discrete time with pytest.raises(ct.ControlArgument, match="LTI system must be"): @@ -540,13 +540,13 @@ def testDLQR_warning(self): with pytest.warns(UserWarning): (K, S, E) = dlqr(A, B, Q, R, N) - @pytest.mark.parametrize('have_slycot', - [pytest.param(True, marks=pytest.mark.slycot), - pytest.param(False, marks=pytest.mark.noslycot)]) + @pytest.mark.parametrize('have_slicot', + [pytest.param(True, marks=pytest.mark.slicot), + pytest.param(False, marks=pytest.mark.noslicot)]) @pytest.mark.parametrize("method", - [pytest.param('slycot', marks=pytest.mark.slycot), + [pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) - def test_care(self, have_slycot, method): + def test_care(self, have_slicot, method): """Test stabilizing and anti-stabilizing feedback, continuous""" A = np.diag([1, -1]) B = np.identity(2) @@ -558,7 +558,7 @@ def test_care(self, have_slycot, method): X, L, G = care(A, B, Q, R, S, E, stabilizing=True, method=method) assert np.all(np.real(L) < 0) - if have_slycot and method=='slycot': + if have_slicot and method=='slicot': X, L, G = care(A, B, Q, R, S, E, stabilizing=False, method=method) assert np.all(np.real(L) > 0) else: @@ -567,7 +567,7 @@ def test_care(self, have_slycot, method): @pytest.mark.parametrize( "stabilizing", - [True, pytest.param(False, marks=pytest.mark.slycot)]) + [True, pytest.param(False, marks=pytest.mark.slicot)]) def test_dare(self, stabilizing): """Test stabilizing and anti-stabilizing feedback, discrete""" A = np.diag([0.5, 2]) @@ -790,10 +790,10 @@ def test_statefbk_iosys_unused(self): np.testing.assert_allclose(clsys0_lin.A, clsys2_lin.A) - @pytest.mark.parametrize('have_slycot', - [pytest.param(True, marks=pytest.mark.slycot), - pytest.param(False, marks=pytest.mark.noslycot)]) - def test_lqr_integral_continuous(self, have_slycot): + @pytest.mark.parametrize('have_slicot', + [pytest.param(True, marks=pytest.mark.slicot), + pytest.param(False, marks=pytest.mark.noslicot)]) + def test_lqr_integral_continuous(self, have_slicot): # Generate a continuous-time system for testing sys = ct.rss(4, 4, 2, strictly_proper=True) sys.C = np.eye(4) # reset output to be full state @@ -855,7 +855,7 @@ def test_lqr_integral_continuous(self, have_slycot): assert all(np.real(clsys.poles()) < 0) # Make sure controller infinite zero frequency gain - if have_slycot: + if have_slicot: ctrl_tf = tf(ctrl) assert abs(ctrl_tf(1e-9)[0][0]) > 1e6 assert abs(ctrl_tf(1e-9)[1][1]) > 1e6 diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 9b3c677fe..cf3ed0aff 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -229,7 +229,7 @@ def test_zero_empty(self): sys = _convert_to_statespace(TransferFunction([1], [1, 2, 1])) np.testing.assert_array_equal(sys.zeros(), np.array([])) - @pytest.mark.slycot + @pytest.mark.slicot def test_zero_siso(self, sys222): """Evaluate the zeros of a SISO system.""" # extract only first input / first output system of sys222. This system is denoted sys111 @@ -259,7 +259,7 @@ def test_zero_mimo_sys222_square(self, sys222): true_z = np.sort([-10.568501, 3.368501]) np.testing.assert_array_almost_equal(z, true_z) - @pytest.mark.slycot + @pytest.mark.slicot def test_zero_mimo_sys623_non_square(self, sys623): """Evaluate the zeros of a non square MIMO system.""" @@ -406,7 +406,7 @@ def test_add_sub_mimo_siso(self): ss2tf(result).minreal(), ) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.parametrize( "left, right, expected", [ @@ -481,7 +481,7 @@ def test_mul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.parametrize( "left, right, expected", [ @@ -556,7 +556,7 @@ def test_rmul_mimo_siso(self, left, right, expected): ss2tf(result).minreal(), ) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.parametrize("power", [0, 1, 3, -3]) @pytest.mark.parametrize("sysname", ["sys222", "sys322"]) def test_pow(self, request, sysname, power): @@ -575,7 +575,7 @@ def test_pow(self, request, sysname, power): np.testing.assert_allclose(expected.C, result.C) np.testing.assert_allclose(expected.D, result.D) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.parametrize("order", ["left", "right"]) @pytest.mark.parametrize("sysname", ["sys121", "sys222", "sys322"]) def test_pow_inv(self, request, sysname, order): @@ -599,7 +599,7 @@ def test_pow_inv(self, request, sysname, order): # Check that the output is the same as the input np.testing.assert_allclose(R.outputs, U) - @pytest.mark.slycot + @pytest.mark.slicot def test_truediv(self, sys222, sys322): """Test state space truediv""" for sys in [sys222, sys322]: @@ -618,7 +618,7 @@ def test_truediv(self, sys222, sys322): ss2tf(result).minreal(), ) - @pytest.mark.slycot + @pytest.mark.slicot def test_rtruediv(self, sys222, sys322): """Test state space rtruediv""" for sys in [sys222, sys322]: @@ -719,7 +719,7 @@ def test_freq_resp(self): mag, phase, omega = sys.freqresp(true_omega) np.testing.assert_almost_equal(mag, true_mag) - @pytest.mark.slycot + @pytest.mark.slicot def test_minreal(self): """Test a minreal model reduction.""" # A = [-2, 0.5, 0; 0.5, -0.3, 0; 0, 0, -0.1] @@ -899,8 +899,8 @@ def test_dc_gain_integrator(self, outputs, inputs, dt): try: np.testing.assert_array_equal(dc, sys.dcgain()) except NotImplementedError: - # Skip MIMO tests if there is no slycot - pytest.skip("slycot required for MIMO dcgain") + # Skip MIMO tests if there is no slicot + pytest.skip("slicot required for MIMO dcgain") def test_scalar_static_gain(self): """Regression: can we create a scalar static gain? @@ -1514,7 +1514,7 @@ def dt_siso(self, request): name, systype, sysargs, dt, refgpeak, reffpeak = request.param return ct.c2d(systype(*sysargs), dt), refgpeak, reffpeak - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_siso(self, ct_siso): sys, refgpeak, reffpeak = ct_siso @@ -1522,7 +1522,7 @@ def test_linfnorm_ct_siso(self, ct_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_dt_siso(self, dt_siso): sys, refgpeak, reffpeak = dt_siso @@ -1531,7 +1531,7 @@ def test_linfnorm_dt_siso(self, dt_siso): np.testing.assert_allclose(gpeak, refgpeak) np.testing.assert_allclose(fpeak, reffpeak) - @pytest.mark.slycot + @pytest.mark.slicot @pytest.mark.usefixtures('ignore_future_warning') def test_linfnorm_ct_mimo(self, ct_siso): siso, refgpeak, reffpeak = ct_siso @@ -1571,13 +1571,13 @@ def test_params_warning(): # pytest.param(None), # use this one when SLICOT bug is sorted out pytest.param( # remove this one when SLICOT bug is sorted out None, marks=pytest.mark.xfail( - ct.slycot_check(), reason="tf2ss SLICOT bug")), + ct.slicot_check(), reason="tf2ss SLICOT bug")), pytest.param( - 'slycot', marks=[ + 'slicot', marks=[ pytest.mark.xfail( - not ct.slycot_check(), reason="slycot not installed"), + not ct.slicot_check(), reason="slicot not installed"), pytest.mark.xfail( # remove this one when SLICOT bug is sorted out - ct.slycot_check(), reason="tf2ss SLICOT bug")]), + ct.slicot_check(), reason="tf2ss SLICOT bug")]), pytest.param('scipy') ]) def test_tf2ss_unstable(method): @@ -1602,13 +1602,13 @@ def test_tf2ss_unstable(method): np.testing.assert_allclose(tf_poles, ss_poles, rtol=1e-4) -@pytest.mark.parametrize('have_slycot', - [pytest.param(True, marks=pytest.mark.slycot), - pytest.param(False, marks=pytest.mark.noslycot)]) -def test_tf2ss_mimo(have_slycot): +@pytest.mark.parametrize('have_slicot', + [pytest.param(True, marks=pytest.mark.slicot), + pytest.param(False, marks=pytest.mark.noslicot)]) +def test_tf2ss_mimo(have_slicot): sys_tf = ct.tf([[[1], [1, 1, 1]]], [[[1, 1, 1], [1, 2, 1]]]) - if have_slycot: + if have_slicot: sys_ss = ct.ss(sys_tf) np.testing.assert_allclose( np.sort(sys_tf.poles()), np.sort(sys_ss.poles())) diff --git a/control/tests/stochsys_test.py b/control/tests/stochsys_test.py index 20e799643..e980f175a 100644 --- a/control/tests/stochsys_test.py +++ b/control/tests/stochsys_test.py @@ -28,7 +28,7 @@ def check_DLQE(L, P, poles, G, QN, RN): np.testing.assert_almost_equal(poles, poles_expected) @pytest.mark.parametrize("method", [None, - pytest.param('slycot', marks=pytest.mark.slycot), + pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) def test_LQE(method): A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) @@ -71,14 +71,14 @@ def test_lqe_call_format(cdlqe): with pytest.raises(ct.ControlArgument, match="not enough input"): L, P, E = cdlqe(sys.A, sys.C) - # First argument is the wrong type (use SISO for non-slycot tests) + # First argument is the wrong type (use SISO for non-slicot tests) sys_tf = tf(rss(3, 1, 1)) sys_tf.dt = None # treat as either continuous or discrete time with pytest.raises(ct.ControlArgument, match="LTI system must be"): L, P, E = cdlqe(sys_tf, Q, R) @pytest.mark.parametrize("method", [None, - pytest.param('slycot', marks=pytest.mark.slycot), + pytest.param('slicot', marks=pytest.mark.slicot), 'scipy']) def test_DLQE(method): A, G, C, QN, RN = (np.array([[X]]) for X in [0., .1, 1., 10., 2.]) diff --git a/control/tests/timeplot_test.py b/control/tests/timeplot_test.py index ea0a290c9..432c3d16a 100644 --- a/control/tests/timeplot_test.py +++ b/control/tests/timeplot_test.py @@ -236,7 +236,7 @@ def test_axes_setup(): sys_3x1 = ct.rss(4, 3, 1) -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.usefixtures('mplcleanup') def test_legend_map(): sys_mimo = ct.tf2ss( @@ -370,7 +370,7 @@ def test_list_responses(resp_fcn): assert cplt.lines[row, col][1].get_color() == 'tab:orange' -@pytest.mark.slycot +@pytest.mark.slicot @pytest.mark.usefixtures('mplcleanup') def test_linestyles(): # Check to make sure we can change line styles diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 16ee01a3d..f3d180781 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -453,7 +453,7 @@ def test_step_info(self, tsystem, systype, time_2d, yfinal): @pytest.mark.parametrize( "tsystem", ['mimo_ss_step_matlab', - pytest.param('mimo_tf_step_info', marks=pytest.mark.slycot)], + pytest.param('mimo_tf_step_info', marks=pytest.mark.slicot)], indirect=["tsystem"]) def test_step_info_mimo(self, tsystem, systype, yfinal): """Test step info for MIMO systems.""" @@ -798,7 +798,7 @@ def test_lsim_double_integrator(self, u, x0, xtrue): np.testing.assert_array_almost_equal(yout, ytrue, decimal=6) - @pytest.mark.slycot + @pytest.mark.slicot def test_step_robustness(self): "Test robustness os step_response against denomiantors: gh-240" # Create 2 input, 2 output system @@ -901,9 +901,9 @@ def test_default_timevector_functions_d(self, fun, dt): "siso_dtf2", "siso_ss2_dtnone", # undetermined timebase "mimo_ss2", # MIMO - pytest.param("mimo_tf2", marks=pytest.mark.slycot), + pytest.param("mimo_tf2", marks=pytest.mark.slicot), "mimo_dss1", - pytest.param("mimo_dtf1", marks=pytest.mark.slycot), + pytest.param("mimo_dtf1", marks=pytest.mark.slicot), ], indirect=True) @pytest.mark.parametrize("fun", [step_response, @@ -1034,7 +1034,7 @@ def test_time_series_data_convention_2D(self, tsystem): def p(*args): # convenience for parametrize below - return pytest.param(*args, marks=pytest.mark.slycot) + return pytest.param(*args, marks=pytest.mark.slicot) @pytest.mark.usefixtures("editsdefaults") @pytest.mark.parametrize("fcn, nstate, nout, ninp, squeeze, shape1, shape2", [ diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index a9be040ab..0d81a1b8e 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -997,7 +997,7 @@ def test_minreal_4(self): np.testing.assert_allclose(hm.num[0][0], hr.num[0][0]) np.testing.assert_allclose(hr.dt, hm.dt) - @pytest.mark.slycot + @pytest.mark.slicot def test_state_space_conversion_mimo(self): """Test conversion of a single input, two-output state-space system against the same TF""" diff --git a/control/xferfcn.py b/control/xferfcn.py index 8e51534d7..4fa373bf1 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1519,8 +1519,8 @@ def _convert_to_transfer_function( elif isinstance(sys, StateSpace): if 0 == sys.nstates: - # Slycot doesn't like static SS->TF conversion, so handle - # it first. Can't join this with the no-Slycot branch, + # slicot doesn't like static SS->TF conversion, so handle + # it first. Can't join this with the no-slicot branch, # since that doesn't handle general MIMO systems num = [[[sys.D[i, j]] for j in range(sys.ninputs)] for i in range(sys.noutputs)] @@ -1534,22 +1534,24 @@ def _convert_to_transfer_function( for i in range(sys.noutputs)] try: - # Use Slycot to make the transformation + # Use slicot to make the transformation # Make sure to convert system matrices to NumPy arrays - from slycot import tb04ad + from .slicot_compat import tb04ad tfout = tb04ad( sys.nstates, sys.ninputs, sys.noutputs, array(sys.A), array(sys.B), array(sys.C), array(sys.D), tol1=0.0) for i in range(sys.noutputs): + # index contains the degree of each row's denominator + deg_i = tfout[4][i] for j in range(sys.ninputs): - num[i][j] = list(tfout[6][i, j, :]) + num[i][j] = list(tfout[6][i, j, :deg_i+1]) # Each transfer function matrix row # has a common denominator. - den[i][j] = list(tfout[5][i, :]) + den[i][j] = list(tfout[5][i, :deg_i+1]) except ImportError: - # If slycot not available, do conversion using sp.signal.ss2tf + # If slicot not available, do conversion using sp.signal.ss2tf for j in range(sys.ninputs): num_j, den_j = sp.signal.ss2tf( sys.A, sys.B, sys.C, sys.D, input=j) diff --git a/examples/cds110-L3_lti-systems.ipynb b/examples/cds110-L3_lti-systems.ipynb index 652bb1216..df164c5ec 100644 --- a/examples/cds110-L3_lti-systems.ipynb +++ b/examples/cds110-L3_lti-systems.ipynb @@ -389,14 +389,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "try:\n", - " G = ct.ss2tf(sys, name='u to q1, q2')\n", - "except ct.ControlMIMONotImplemented:\n", - " # Create SISO transfer functions, in case we don't have slycot\n", - " G = ct.ss2tf(sys[0, 0], name='u to q1')\n", - "print(G)" - ] + "source": "try:\n G = ct.ss2tf(sys, name='u to q1, q2')\nexcept ct.ControlMIMONotImplemented:\n # Create SISO transfer functions, in case we don't have slicot\n G = ct.ss2tf(sys[0, 0], name='u to q1')\nprint(G)" }, { "cell_type": "code", @@ -512,4 +505,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/cds110-L8a_maglev-limits.ipynb b/examples/cds110-L8a_maglev-limits.ipynb index 5a7473ade..6ef22c31e 100644 --- a/examples/cds110-L8a_maglev-limits.ipynb +++ b/examples/cds110-L8a_maglev-limits.ipynb @@ -248,16 +248,7 @@ "id": "4df561a2-16aa-41b0-9971-f8c151467730", "metadata": {}, "outputs": [], - "source": [ - "# Bode integral calculation\n", - "omega = np.linspace(0, 1e6, 100000)\n", - "for name, sys in zip(['C1', 'C2', 'C3'], [magS1, magS2, magS3]):\n", - " freqresp = ct.frequency_response(sys, omega)\n", - " bodeint = np.trapz(np.log(freqresp.magnitude), omega)\n", - " print(\"Bode integral for\", name, \"=\", bodeint)\n", - "\n", - "print(\"pi * sum[ Re(pk) ]\", pi * np.sum(magP.poles()[magP.poles().real > 0]))" - ] + "source": "# Bode integral calculation\nomega = np.linspace(0, 1e6, 100000)\nfor name, sys in zip(['C1', 'C2', 'C3'], [magS1, magS2, magS3]):\n freqresp = ct.frequency_response(sys, omega)\n bodeint = sp.integrate.trapezoid(np.log(freqresp.magnitude), omega)\n print(\"Bode integral for\", name, \"=\", bodeint)\n\nprint(\"pi * sum[ Re(pk) ]\", pi * np.sum(magP.poles()[magP.poles().real > 0]))" }, { "cell_type": "code", @@ -275,4 +266,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/cds112-L6_stochastic-linsys.ipynb b/examples/cds112-L6_stochastic-linsys.ipynb index 3efc158cb..6425cf44f 100644 --- a/examples/cds112-L6_stochastic-linsys.ipynb +++ b/examples/cds112-L6_stochastic-linsys.ipynb @@ -89,11 +89,7 @@ "id": "23319dc6", "metadata": {}, "outputs": [], - "source": [ - "# Calculate the sample properties and make sure they match\n", - "print(\"mean(V) [0.0] = \", np.mean(V))\n", - "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" - ] + "source": "# Calculate the sample properties and make sure they match\nprint(\"mean(V) [0.0] = \", np.mean(V))\nprint(f\"cov(V) * dt [{Q.item():.3g}] = \", np.round(np.cov(V), decimals=3) * dt)" }, { "cell_type": "code", @@ -325,4 +321,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/python-control_tutorial.ipynb b/examples/python-control_tutorial.ipynb index 6ac127758..68e6da325 100644 --- a/examples/python-control_tutorial.ipynb +++ b/examples/python-control_tutorial.ipynb @@ -1218,29 +1218,11 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "280d8d0e-38bc-484c-8ed5-fd6a7f2b56b5", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Control version: 0.10.1.dev324+g2fd3802a.d20241218\n", - "Slycot version: 0.6.0\n", - "NumPy version: 2.2.0\n" - ] - } - ], - "source": [ - "print(\"Control version:\", ct.__version__)\n", - "if ct.slycot_check():\n", - " import slycot\n", - " print(\"Slycot version:\", slycot.__version__)\n", - "else:\n", - " print(\"Slycot version: not installed\")\n", - "print(\"NumPy version:\", np.__version__)" - ] + "outputs": [], + "source": "print(\"Control version:\", ct.__version__)\nif ct.slicot_check():\n import slicot\n print(\"Slicot version:\", slicot.__version__)\nelse:\n print(\"Slicot version: not installed\")\nprint(\"NumPy version:\", np.__version__)" } ], "metadata": { @@ -1264,4 +1246,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/slycot-import-test.py b/examples/slicot-import-test.py similarity index 68% rename from examples/slycot-import-test.py rename to examples/slicot-import-test.py index 9c92fd2dc..0bdcb4991 100644 --- a/examples/slycot-import-test.py +++ b/examples/slicot-import-test.py @@ -1,12 +1,12 @@ -""" slycot-import-test.py +""" slicot-import-test.py -Simple example script to test Slycot import +Simple example script to test slicot import RMM, 28 May 09 """ import numpy as np import control as ct -from control.exception import slycot_check +from control.exception import slicot_check # Parameters defining the system m = 250.0 # system mass @@ -19,18 +19,18 @@ C = np.array([[1., 0, 1.]]) sys = ct.ss(A, B, C, 0) -# Python control may be used without slycot, for example for a pole placement. +# Python control may be used without slicot, for example for a pole placement. # Eigenvalue placement w = [-3, -2, -1] K = ct.place(A, B, w) print("[python-control (from scipy)] K = ", K) print("[python-control (from scipy)] eigs = ", np.linalg.eig(A - B*K)[0]) -# Before using one of its routine, check that slycot is installed. +# Before using one of its routine, check that slicot is installed. w = np.array([-3, -2, -1]) -if slycot_check(): +if slicot_check(): # Import routine sb01bd used for pole placement. - from slycot import sb01bd + from control.slicot_compat import sb01bd n = 3 # Number of states m = 1 # Number of inputs @@ -38,7 +38,7 @@ alpha = 1 # Maximum threshold for eigen values dico = 'D' # Discrete system _, _, _, _, _, K, _ = sb01bd(n, m, npp, alpha, A, B, w, dico, tol=0.0, ldwork=None) - print("[slycot] K = ", K) - print("[slycot] eigs = ", np.linalg.eig(A + B @ K)[0]) + print("[slicot] K = ", K) + print("[slicot] eigs = ", np.linalg.eig(A + B @ K)[0]) else: - print("Slycot is not installed.") + print("slicot is not installed.") diff --git a/examples/stochresp.ipynb b/examples/stochresp.ipynb index dda6bb501..17a865d32 100644 --- a/examples/stochresp.ipynb +++ b/examples/stochresp.ipynb @@ -97,24 +97,11 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": null, "id": "23319dc6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mean(V) [0.0] = 0.17348786109316244\n", - "cov(V) * dt [0.1] = 0.09633133133133133\n" - ] - } - ], - "source": [ - "# Calculate the sample properties and make sure they match\n", - "print(\"mean(V) [0.0] = \", np.mean(V))\n", - "print(\"cov(V) * dt [%0.3g] = \" % Q, np.round(np.cov(V), decimals=3) * dt)" - ] + "outputs": [], + "source": "# Calculate the sample properties and make sure they match\nprint(\"mean(V) [0.0] = \", np.mean(V))\nprint(f\"cov(V) * dt [{Q.item():.3g}] = \", np.round(np.cov(V), decimals=3) * dt)" }, { "cell_type": "markdown", @@ -161,27 +148,11 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": null, "id": "d31ce324", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* mean(Y) [0] = 0.165\n", - "* cov(Y) [0.05] = 0.0151\n" - ] - } - ], - "source": [ - "# Compare static properties to what we expect analytically\n", - "def r(tau):\n", - " return c**2 * Q / (2 * a) * exp(-a * abs(tau))\n", - " \n", - "print(\"* mean(Y) [%0.3g] = %0.3g\" % (0, np.mean(Y)))\n", - "print(\"* cov(Y) [%0.3g] = %0.3g\" % (r(0), np.cov(Y)))" - ] + "outputs": [], + "source": "# Compare static properties to what we expect analytically\ndef r(tau):\n return c**2 * Q / (2 * a) * exp(-a * abs(tau))\n \nprint(f\"* mean(Y) [0] = {np.mean(Y):.3g}\")\nprint(f\"* cov(Y) [{r(0).item():.3g}] = {np.cov(Y).item():.3g}\")" }, { "cell_type": "markdown", @@ -289,4 +260,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b76a3731f..eec1d2369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ packages = ["control"] [project.optional-dependencies] test = ["pytest", "pytest-timeout", "ruff", "numpydoc"] -slycot = [ "slycot>=0.4.0" ] +slicot = [ "slicot" ] cvxopt = [ "cvxopt>=1.2.0" ] [project.urls] @@ -56,8 +56,8 @@ filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] markers = [ - "slycot: tests needing slycot", - "noslycot: test needing slycot absent", + "slicot: tests needing slicot", + "noslicot: test needing slicot absent", "cvxopt: tests needing cvxopt", "pandas: tests needing pandas", ]