From dfc0ead207b44652de62c00050316cbe3415196c Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 25 Jan 2025 17:00:45 +0200 Subject: [PATCH 01/22] Handle non-* unused import-related messages Mostly removing imports; in some cases, placate pyflakes with fake evaluation of not-obviously-used name. --- control/canonical.py | 5 ++--- control/dtime.py | 1 - control/exception.py | 3 +++ control/flatsys/bspline.py | 2 +- control/flatsys/flatsys.py | 1 - control/frdata.py | 4 ++-- control/freqplot.py | 1 - control/iosys.py | 1 - control/lti.py | 3 ++- control/mateqn.py | 2 +- control/nichols.py | 1 - control/nlsys.py | 6 ++---- control/optimal.py | 3 +-- control/phaseplot.py | 1 - control/pzmap.py | 5 +---- control/rlocus.py | 5 +---- control/statesp.py | 10 ++++++---- control/stochsys.py | 9 +++++---- control/sysnorm.py | 1 - control/timeplot.py | 3 --- control/timeresp.py | 2 -- control/xferfcn.py | 8 +++++--- 22 files changed, 32 insertions(+), 45 deletions(-) diff --git a/control/canonical.py b/control/canonical.py index 7be7f88ad..67d3127a9 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -8,9 +8,8 @@ import numpy as np -from numpy import zeros, zeros_like, shape, poly, iscomplex, vstack, hstack, \ - transpose, empty, finfo, float64 -from numpy.linalg import solve, matrix_rank, eig +from numpy import zeros_like, poly, transpose +from numpy.linalg import solve, matrix_rank from scipy.linalg import schur diff --git a/control/dtime.py b/control/dtime.py index 39b207e02..6d1545fc0 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -48,7 +48,6 @@ """ from .iosys import isctime -from .statesp import StateSpace __all__ = ['sample_system', 'c2d'] diff --git a/control/exception.py b/control/exception.py index add5d01ae..79ecb6ef3 100644 --- a/control/exception.py +++ b/control/exception.py @@ -72,6 +72,7 @@ def slycot_check(): if slycot_installed is None: try: import slycot + slycot # pyflakes slycot_installed = True except: slycot_installed = False @@ -86,6 +87,7 @@ def pandas_check(): if pandas_installed is None: try: import pandas + pandas # pyflakes pandas_installed = True except: pandas_installed = False @@ -99,6 +101,7 @@ def cvxopt_check(): if cvxopt_installed is None: try: import cvxopt + cvxopt # pyflakes cvxopt_installed = True except: cvxopt_installed = False diff --git a/control/flatsys/bspline.py b/control/flatsys/bspline.py index c771beb59..d42cb4074 100644 --- a/control/flatsys/bspline.py +++ b/control/flatsys/bspline.py @@ -8,7 +8,7 @@ import numpy as np from .basis import BasisFamily -from scipy.interpolate import BSpline, splev +from scipy.interpolate import BSpline class BSplineFamily(BasisFamily): """B-spline basis functions. diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index 5818d118b..a330d35b3 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -245,7 +245,6 @@ def flatsys(*args, updfcn=None, outfcn=None, **kwargs): """ from .linflat import LinearFlatSystem from ..statesp import StateSpace - from ..iosys import _process_iosys_keywords if len(args) == 1 and isinstance(args[0], StateSpace): # We were passed a linear system, so call linflat diff --git a/control/frdata.py b/control/frdata.py index 1200bfffa..195d73bfb 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -15,8 +15,8 @@ from warnings import warn import numpy as np -from numpy import absolute, angle, array, empty, eye, imag, linalg, ones, \ - real, sort, where +from numpy import absolute, array, empty, eye, imag, linalg, ones, \ + real, sort from scipy.interpolate import splev, splprep from . import config diff --git a/control/freqplot.py b/control/freqplot.py index 456431f38..994d7e3be 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -11,7 +11,6 @@ import itertools import math import warnings -from os.path import commonprefix import matplotlib as mpl import matplotlib.pyplot as plt diff --git a/control/iosys.py b/control/iosys.py index 373bc2111..293657319 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -8,7 +8,6 @@ import re from copy import deepcopy -from warnings import warn import numpy as np diff --git a/control/lti.py b/control/lti.py index cb785ca5f..b5f634169 100644 --- a/control/lti.py +++ b/control/lti.py @@ -7,7 +7,8 @@ import numpy as np import math -from numpy import real, angle, abs +# todo: override built-in abs +from numpy import real, abs from warnings import warn from . import config from .iosys import InputOutputSystem diff --git a/control/mateqn.py b/control/mateqn.py index b73abdfcc..64d5f86da 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -37,7 +37,7 @@ import warnings import numpy as np -from numpy import copy, eye, dot, finfo, inexact, atleast_2d +from numpy import eye, finfo, inexact import scipy as sp from scipy.linalg import eigvals, solve diff --git a/control/nichols.py b/control/nichols.py index ac42c9c37..b24482045 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -21,7 +21,6 @@ from .ctrlplot import ControlPlot, _get_line_labels, _process_ax_keyword, \ _process_legend_keywords, _process_line_labels, _update_plot_title from .ctrlutil import unwrap -from .freqplot import _default_frequency_range, _freqplot_defaults from .lti import frequency_response from .statesp import StateSpace from .xferfcn import TransferFunction diff --git a/control/nlsys.py b/control/nlsys.py index 7683d3382..a86e06296 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -18,7 +18,6 @@ """ -import copy from warnings import warn import numpy as np @@ -26,7 +25,7 @@ from . import config from .iosys import InputOutputSystem, _parse_spec, _process_iosys_keywords, \ - _process_signal_list, common_timebase, iosys_repr, isctime, isdtime + common_timebase, iosys_repr, isctime, isdtime from .timeresp import _check_convert_array, _process_time_response, \ TimeResponseData, TimeResponseList @@ -2474,8 +2473,7 @@ def interconnect( `outputs`, for more natural naming of SISO systems. """ - from .statesp import LinearICSystem, StateSpace, _convert_to_statespace - from .xferfcn import TransferFunction + from .statesp import LinearICSystem, StateSpace dt = kwargs.pop('dt', None) # bypass normal 'dt' processing name, inputs, outputs, states, _ = _process_iosys_keywords(kwargs) diff --git a/control/optimal.py b/control/optimal.py index 77cfd370e..dad1a9feb 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -23,8 +23,7 @@ import time from . import config -from .exception import ControlNotImplemented -from .iosys import _process_indices, _process_labels, \ +from .iosys import _process_labels, \ _process_control_disturbance_indices diff --git a/control/phaseplot.py b/control/phaseplot.py index e6123bc0e..f71e45874 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -38,7 +38,6 @@ from . import config from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \ _process_ax_keyword, _update_plot_title -from .exception import ControlNotImplemented from .nlsys import NonlinearIOSystem, find_operating_point, \ input_output_response diff --git a/control/pzmap.py b/control/pzmap.py index f1d0ecae9..b62e0dcf0 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -11,21 +11,18 @@ import itertools import warnings -from math import pi import matplotlib.pyplot as plt import numpy as np -from numpy import cos, exp, imag, linspace, real, sin, sqrt +from numpy import imag, real from . import config from .config import _process_legacy_keyword from .ctrlplot import ControlPlot, _get_color, _get_color_offset, \ _get_line_labels, _process_ax_keyword, _process_legend_keywords, \ _process_line_labels, _update_plot_title -from .freqplot import _freqplot_defaults from .grid import nogrid, sgrid, zgrid from .iosys import isctime, isdtime -from .lti import LTI from .statesp import StateSpace from .xferfcn import TransferFunction diff --git a/control/rlocus.py b/control/rlocus.py index e5f61f914..2ed88972e 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -16,17 +16,14 @@ # import warnings -from functools import partial -import matplotlib.pyplot as plt import numpy as np import scipy.signal # signal processing toolbox -from numpy import array, imag, poly1d, real, vstack, zeros_like +from numpy import poly1d, vstack, zeros_like from . import config from .ctrlplot import ControlPlot from .exception import ControlMIMONotImplemented -from .iosys import isdtime from .lti import LTI from .xferfcn import _convert_to_transfer_function diff --git a/control/statesp.py b/control/statesp.py index 44fe8b605..28e9fc95c 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -16,14 +16,14 @@ import math import sys from collections.abc import Iterable -from copy import deepcopy from warnings import warn import numpy as np import scipy as sp import scipy.linalg +# todo: check override of built-in any from numpy import any, array, asarray, concatenate, cos, delete, empty, \ - exp, eye, isinf, ones, pad, sin, squeeze, zeros + exp, eye, isinf, pad, sin, squeeze, zeros from numpy.linalg import LinAlgError, eigvals, matrix_rank, solve from numpy.random import rand, randn from scipy.signal import StateSpace as signalStateSpace @@ -33,13 +33,15 @@ from . import bdalg from .exception import ControlMIMONotImplemented, ControlSlycot, slycot_check from .frdata import FrequencyResponseData -from .iosys import InputOutputSystem, NamedSignal, _process_dt_keyword, \ +from .iosys import InputOutputSystem, NamedSignal, \ _process_iosys_keywords, _process_signal_list, _process_subsys_index, \ - common_timebase, iosys_repr, isdtime, issiso + common_timebase, issiso from .lti import LTI, _process_frequency_response from .nlsys import InterconnectedSystem, NonlinearIOSystem import control +array # pyflakes + try: from slycot import ab13dd except ImportError: diff --git a/control/stochsys.py b/control/stochsys.py index b31083f19..bf5d5e8ef 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -16,14 +16,15 @@ __maintainer__ = "Richard Murray" __email__ = "murray@cds.caltech.edu" +import warnings + import numpy as np import scipy as sp from math import sqrt -from .statesp import StateSpace from .lti import LTI -from .iosys import InputOutputSystem, isctime, isdtime, _process_indices, \ - _process_labels, _process_control_disturbance_indices +from .iosys import (isctime, isdtime, _process_labels, + _process_control_disturbance_indices) from .nlsys import NonlinearIOSystem from .mateqn import care, dare, _check_shape from .statesp import StateSpace, _ssmatrix @@ -460,7 +461,7 @@ def create_estimator_iosystem( # Set the input and direct matrices B = sys.B[:, ctrl_idx] if not np.allclose(sys.D, 0): - raise NotImplemented("nonzero 'D' matrix not yet implemented") + raise NotImplementedError("nonzero 'D' matrix not yet implemented") # Set the output matrices if C is not None: diff --git a/control/sysnorm.py b/control/sysnorm.py index 6737dc5c0..680bb4b15 100644 --- a/control/sysnorm.py +++ b/control/sysnorm.py @@ -12,7 +12,6 @@ """ import numpy as np -import scipy as sp import numpy.linalg as la import warnings diff --git a/control/timeplot.py b/control/timeplot.py index 1c7efe894..6fde8c67a 100644 --- a/control/timeplot.py +++ b/control/timeplot.py @@ -11,7 +11,6 @@ import itertools from warnings import warn -import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np @@ -178,8 +177,6 @@ def time_response_plot( """ from .ctrlplot import _process_ax_keyword, _process_line_labels - from .iosys import InputOutputSystem - from .timeresp import TimeResponseData # # Process keywords and set defaults diff --git a/control/timeresp.py b/control/timeresp.py index 67641d239..e62812634 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -1838,8 +1838,6 @@ def initial_response( >>> T, yout = ct.initial_response(G) """ - from .lti import LTI - # Create the time and input vectors if T is None or np.asarray(T).size == 1: T = _default_time_vector(sysdata, N=T_num, tfinal=T, is_step=False) diff --git a/control/xferfcn.py b/control/xferfcn.py index 4a8fd4a1c..97a5a1055 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -22,8 +22,8 @@ import numpy as np import scipy as sp -from numpy import angle, array, delete, empty, exp, finfo, float64, ndarray, \ - nonzero, ones, pi, poly, polyadd, polymul, polyval, real, roots, sqrt, \ +from numpy import array, delete, empty, exp, finfo, float64, ndarray, \ + nonzero, ones, poly, polyadd, polymul, polyval, real, roots, sqrt, \ squeeze, where, zeros from scipy.signal import TransferFunction as signalTransferFunction from scipy.signal import cont2discrete, tf2zpk, zpk2tf @@ -33,9 +33,11 @@ from .exception import ControlMIMONotImplemented from .frdata import FrequencyResponseData from .iosys import InputOutputSystem, NamedSignal, _process_iosys_keywords, \ - _process_subsys_index, common_timebase, isdtime + _process_subsys_index, common_timebase from .lti import LTI, _process_frequency_response +float64 # pyflakes + __all__ = ['TransferFunction', 'tf', 'zpk', 'ss2tf', 'tfdata'] From eb70132a42e72b6adbf76e8e09191ff22799fc1d Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 09:29:28 +0200 Subject: [PATCH 02/22] Silence ruff warnings in __init__.py --- control/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/control/__init__.py b/control/__init__.py index 1aaaa42e8..eb4eb1312 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -70,6 +70,11 @@ # Import functions from within the control system library # Note: the functions we use are specified as __all__ variables in the modules +# don't warn about `import *` +# ruff: noqa: F403 +# don't warn about unknown names; they come `import *` +# ruff: noqa: F405 + # Input/output system modules from .iosys import * from .nlsys import * @@ -106,8 +111,8 @@ from .sysnorm import * # Allow access to phase_plane functions as ct.phaseplot.fcn or ct.pp.fcn -from . import phaseplot -from . import phaseplot as pp +from . import phaseplot as phaseplot +pp = phaseplot # Exceptions from .exception import * From c135d2ffbe1080e4fa3b64010bcb879b967ad9fe Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 09:30:09 +0200 Subject: [PATCH 03/22] Make flatsys/__init__.py ruff-clean --- control/flatsys/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/control/flatsys/__init__.py b/control/flatsys/__init__.py index c6934d825..16374b589 100644 --- a/control/flatsys/__init__.py +++ b/control/flatsys/__init__.py @@ -61,15 +61,17 @@ """ # Basis function families -from .basis import BasisFamily -from .poly import PolyFamily -from .bezier import BezierFamily -from .bspline import BSplineFamily +from .basis import BasisFamily as BasisFamily +from .poly import PolyFamily as PolyFamily +from .bezier import BezierFamily as BezierFamily +from .bspline import BSplineFamily as BSplineFamily # Classes -from .systraj import SystemTrajectory -from .flatsys import FlatSystem, flatsys -from .linflat import LinearFlatSystem +from .systraj import SystemTrajectory as SystemTrajectory +from .flatsys import FlatSystem as FlatSystem +from .flatsys import flatsys as flatsys +from .linflat import LinearFlatSystem as LinearFlatSystem # Package functions -from .flatsys import point_to_point, solve_flat_ocp +from .flatsys import point_to_point as point_to_point +from .flatsys import solve_flat_ocp as solve_flat_ocp From c6dbe07f94053824ecc9f145323cc60692f9c479 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 09:32:12 +0200 Subject: [PATCH 04/22] Silence ruff warnings in matlab/__init__.py --- control/matlab/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/control/matlab/__init__.py b/control/matlab/__init__.py index 98e6babc7..dca522ec5 100644 --- a/control/matlab/__init__.py +++ b/control/matlab/__init__.py @@ -50,6 +50,9 @@ """ +# Silence unused imports (F401), * imports (F403), unknown symbols (F405) +# ruff: noqa: F401, F403, F405 + # Import MATLAB-like functions that are defined in other packages from scipy.signal import zpk2ss, ss2zpk, tf2zpk, zpk2tf from numpy import linspace, logspace From 9427eecea9b5b89356ce70b1dac05c609ec016b9 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 09:33:50 +0200 Subject: [PATCH 05/22] Turn f-strings without format-parameters into normal strings (ruff) --- control/nlsys.py | 14 +++++++------- control/phaseplot.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index a86e06296..577bfd45d 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -810,7 +810,7 @@ def cxn_string(signal, gain, first): return (" - " if not first else "-") + \ f"{abs(gain)} * {signal}" - out += f"\nConnections:\n" + out += "\nConnections:\n" for i in range(len(input_list)): first = True cxn = f"{input_list[i]} <- " @@ -830,7 +830,7 @@ def cxn_string(signal, gain, first): cxn, width=78, initial_indent=" * ", subsequent_indent=" ")) + "\n" - out += f"\nOutputs:\n" + out += "\nOutputs:\n" for i in range(len(self.output_labels)): first = True cxn = f"{self.output_labels[i]} <- " @@ -2535,7 +2535,7 @@ def interconnect( # This includes signal lists such as ('sysname', ['sig1', 'sig2', ...]) # as well as slice-based specifications such as 'sysname.signal[i:j]'. # - dprint(f"Pre-processing connections:") + dprint("Pre-processing connections:") new_connections = [] for connection in connections: dprint(f" parsing {connection=}") @@ -2574,7 +2574,7 @@ def interconnect( # dprint(f"Pre-processing input connections: {inplist}") if not isinstance(inplist, list): - dprint(f" converting inplist to list") + dprint(" converting inplist to list") inplist = [inplist] new_inplist, new_inputs = [], [] if inplist_none else inputs @@ -2637,7 +2637,7 @@ def interconnect( else: if isinstance(connection, list): # Passed a list => create input map - dprint(f" detected input list") + dprint(" detected input list") signal_list = [] for spec in connection: isys, indices, gain = _parse_spec(syslist, spec, 'input') @@ -2663,7 +2663,7 @@ def interconnect( # dprint(f"Pre-processing output connections: {outlist}") if not isinstance(outlist, list): - dprint(f" converting outlist to list") + dprint(" converting outlist to list") outlist = [outlist] new_outlist, new_outputs = [], [] if outlist_none else outputs for iout, connection in enumerate(outlist): @@ -2740,7 +2740,7 @@ def _find_output_or_input_signal(spec): if isinstance(connection, list): # Passed a list => create input map - dprint(f" detected output list") + dprint(" detected output list") signal_list = [] for spec in connection: signal_list += _find_output_or_input_signal(spec) diff --git a/control/phaseplot.py b/control/phaseplot.py index f71e45874..fa0c05efa 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -1090,9 +1090,9 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, # Get parameters to pass to function if parms: warnings.warn( - f"keyword 'parms' is deprecated; use 'params'", FutureWarning) + "keyword 'parms' is deprecated; use 'params'", FutureWarning) if params: - raise ControlArgument(f"duplicate keywords 'parms' and 'params'") + raise ControlArgument("duplicate keywords 'parms' and 'params'") else: params = parms From 5ba2ad52833cf3f73f510ce3017eaf0e5e1416ac Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 09:53:06 +0200 Subject: [PATCH 06/22] Fix ruff warnings in robust.py --- control/robust.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/control/robust.py b/control/robust.py index f9283af48..3e44c1bb3 100644 --- a/control/robust.py +++ b/control/robust.py @@ -42,9 +42,8 @@ # External packages and modules import numpy as np import warnings -from .exception import * +from .exception import ControlSlycot from .statesp import StateSpace -from .statefbk import * def h2syn(P, nmeas, ncon): @@ -98,12 +97,6 @@ def h2syn(P, nmeas, ncon): # Check for ss system object, need a utility for this? # TODO: Check for continous or discrete, only continuous supported right now - # if isCont(): - # dico = 'C' - # elif isDisc(): - # dico = 'D' - # else: - dico = 'C' try: from slycot import sb10hd @@ -186,12 +179,6 @@ def hinfsyn(P, nmeas, ncon): # Check for ss system object, need a utility for this? # TODO: Check for continous or discrete, only continuous supported right now - # if isCont(): - # dico = 'C' - # elif isDisc(): - # dico = 'D' - # else: - dico = 'C' try: from slycot import sb10ad From 67fa35d584e82a134796c9d1e80b584d4745c49f Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:45:59 +0200 Subject: [PATCH 07/22] Apply ruff pyflakes rules to control library only --- pyproject.toml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 649dcad5d..46d41fbf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,14 @@ filterwarnings = [ "error:.*matrix subclass:PendingDeprecationWarning", ] -[tool.ruff.lint] -select = ['D', 'E', 'W', 'DOC'] +[tool.ruff] + +# TODO: expand to cover all code +include = ['control/**.py'] +exclude = ['control/tests/*.py'] -[tool.ruff.lint.pydocstyle] -convention = 'numpy' +[tool.ruff.lint] +select = [ + 'F', # pyflakes + # todo: add more as needed +] From 44db1264af4bef8cd5899ccc6e1cda56f8675054 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:46:42 +0200 Subject: [PATCH 08/22] Import `ControlArgument` in flatsys.py, with test --- control/flatsys/flatsys.py | 1 + control/tests/flatsys_test.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index a330d35b3..d00c2b311 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -8,6 +8,7 @@ import warnings from .poly import PolyFamily from .systraj import SystemTrajectory +from ..exception import ControlArgument from ..nlsys import NonlinearIOSystem from ..timeresp import _check_convert_array diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py index 5b66edaf5..df30d7090 100644 --- a/control/tests/flatsys_test.py +++ b/control/tests/flatsys_test.py @@ -618,6 +618,11 @@ def test_point_to_point_errors(self): flat_sys, timepts, x0, u0, xf, uf, constraints=[(None, 0, 0, 0)], basis=fs.PolyFamily(8)) + # too few timepoints + with pytest.raises(ct.ControlArgument, match="at least three time points"): + fs.point_to_point( + flat_sys, timepts[:2], x0, u0, xf, uf, basis=fs.PolyFamily(10), cost=cost_fcn) + # Unsolvable optimization constraint = [opt.input_range_constraint(flat_sys, -0.01, 0.01)] with pytest.warns(UserWarning, match="unable to solve"): From 7668d2c70db297f93f7e9a3829d339c4e2b728e7 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:48:25 +0200 Subject: [PATCH 09/22] Fix ruff "unused value" warnings --- control/freqplot.py | 2 -- control/modelsimp.py | 2 +- control/nlsys.py | 4 ++-- control/phaseplot.py | 14 ++++++++------ control/stochsys.py | 8 +++++--- control/xferfcn.py | 1 - 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 994d7e3be..097a05317 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -759,13 +759,11 @@ def _make_line_label(response, output_index, input_index): if plot_magnitude: ax_mag.axhline(y=0 if dB else 1, color='k', linestyle=':', zorder=-20) - mag_ylim = ax_mag.get_ylim() if plot_phase: ax_phase.axhline(y=phase_limit if deg else math.radians(phase_limit), color='k', linestyle=':', zorder=-20) - phase_ylim = ax_phase.get_ylim() # Annotate the phase margin (if it exists) if plot_phase and pm != float('inf') and Wcp != float('nan'): diff --git a/control/modelsimp.py b/control/modelsimp.py index fe519b82d..968003051 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -340,7 +340,7 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): # check if orders is a list or a scalar try: - order = iter(orders) + iter(orders) except TypeError: # if orders is a scalar orders = [orders] diff --git a/control/nlsys.py b/control/nlsys.py index 577bfd45d..39d61c4ee 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -210,7 +210,7 @@ def __mul__(self, other): "can't multiply systems with incompatible inputs and outputs") # Make sure timebase are compatible - dt = common_timebase(other.dt, self.dt) + common_timebase(other.dt, self.dt) # Create a new system to handle the composition inplist = [(0, i) for i in range(other.ninputs)] @@ -242,7 +242,7 @@ def __rmul__(self, other): "inputs and outputs") # Make sure timebase are compatible - dt = common_timebase(self.dt, other.dt) + common_timebase(self.dt, other.dt) # Create a new system to handle the composition inplist = [(0, i) for i in range(self.ninputs)] diff --git a/control/phaseplot.py b/control/phaseplot.py index fa0c05efa..9dd2b8837 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -161,7 +161,6 @@ def phase_plane_plot( # Create copy of kwargs for later checking to find unused arguments initial_kwargs = dict(kwargs) - passed_kwargs = False # Utility function to create keyword arguments def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): @@ -1143,10 +1142,11 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, if scale is None: plt.quiver(x1, x2, dx[:,:,1], dx[:,:,2], angles='xy') elif (scale != 0): + plt.quiver(x1, x2, dx[:,:,0]*np.abs(scale), + dx[:,:,1]*np.abs(scale), angles='xy') #! TODO: optimize parameters for arrows #! TODO: figure out arguments to make arrows show up correctly - xy = plt.quiver(x1, x2, dx[:,:,0]*np.abs(scale), - dx[:,:,1]*np.abs(scale), angles='xy') + # xy = plt.quiver(...) # set(xy, 'LineWidth', PP_arrow_linewidth, 'Color', 'b') #! TODO: Tweak the shape of the plot @@ -1256,15 +1256,17 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, #! TODO: figure out arguments to make arrows show up correctly plt.quiver(x1, x2, dx[:,:,0], dx[:,:,1], angles='xy') elif scale != 0 and Narrows > 0: + plt.quiver(x1, x2, dx[:,:,0]*abs(scale), dx[:,:,1]*abs(scale), + angles='xy') #! TODO: figure out arguments to make arrows show up correctly - xy = plt.quiver(x1, x2, dx[:,:,0]*abs(scale), dx[:,:,1]*abs(scale), - angles='xy') + # xy = plt.quiver(...) # set(xy, 'LineWidth', PP_arrow_linewidth) # set(xy, 'AutoScale', 'off') # set(xy, 'AutoScaleFactor', 0) if scale < 0: - bp = plt.plot(x1, x2, 'b.'); # add dots at base + plt.plot(x1, x2, 'b.'); # add dots at base + # bp = plt.plot(...) # set(bp, 'MarkerSize', PP_arrow_markersize) diff --git a/control/stochsys.py b/control/stochsys.py index bf5d5e8ef..be5110800 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -166,12 +166,14 @@ def lqe(*args, **kwargs): # Get the cross-covariance matrix, if given if (len(args) > index + 2): - NN = np.array(args[index+2], ndmin=2, dtype=float) + # NN = np.array(args[index+2], ndmin=2, dtype=float) raise ControlNotImplemented("cross-covariance not implemented") else: + pass # For future use (not currently used below) - NN = np.zeros((QN.shape[0], RN.shape[1])) + # NN = np.zeros((QN.shape[0], RN.shape[1])) + # Check dimensions of G (needed before calling care()) _check_shape("QN", QN, G.shape[1], G.shape[1]) @@ -290,7 +292,7 @@ def dlqe(*args, **kwargs): # NN = np.zeros(QN.size(0),RN.size(1)) # NG = G @ NN if len(args) > index + 2: - NN = np.array(args[index+2], ndmin=2, dtype=float) + # NN = np.array(args[index+2], ndmin=2, dtype=float) raise ControlNotImplemented("cross-covariance not yet implememented") # Check dimensions of G (needed before calling care()) diff --git a/control/xferfcn.py b/control/xferfcn.py index 97a5a1055..3c7e43d51 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1503,7 +1503,6 @@ def _convert_to_transfer_function( """ from .statesp import StateSpace - kwargs = {} if isinstance(sys, TransferFunction): return sys From ae5a702ba26fff7a95763a66974d2a4d5137f12a Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:49:05 +0200 Subject: [PATCH 10/22] Fixed ruff unused argument warning in acker --- control/statefbk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index 7b96c8015..d53fbcdbd 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -274,12 +274,12 @@ def acker(A, B, poles): """ # Convert the inputs to matrices - a = _ssmatrix(A) - b = _ssmatrix(B) + A = _ssmatrix(A) + B = _ssmatrix(B) # Make sure the system is controllable ct = ctrb(A, B) - if np.linalg.matrix_rank(ct) != a.shape[0]: + if np.linalg.matrix_rank(ct) != A.shape[0]: raise ValueError("System not reachable; pole placement invalid") # Compute the desired characteristic polynomial @@ -288,9 +288,9 @@ def acker(A, B, poles): # Place the poles using Ackermann's method # TODO: compute pmat using Horner's method (O(n) instead of O(n^2)) n = np.size(p) - pmat = p[n-1] * np.linalg.matrix_power(a, 0) + pmat = p[n-1] * np.linalg.matrix_power(A, 0) for i in np.arange(1, n): - pmat = pmat + p[n-i-1] * np.linalg.matrix_power(a, i) + pmat = pmat + p[n-i-1] * np.linalg.matrix_power(A, i) K = np.linalg.solve(ct, pmat) K = K[-1][:] # Extract the last row From f9c5b3b893ad2b0a409de905d0185ca3cb457627 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:49:27 +0200 Subject: [PATCH 11/22] Fix ruff shadowed symbol warning in xferfcn.py --- control/xferfcn.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/xferfcn.py b/control/xferfcn.py index 3c7e43d51..5864bf104 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -221,8 +221,8 @@ def __init__(self, *args, **kwargs): # Determine if the transfer function is static (needed for dt) static = True for arr in [num, den]: - for poly in np.nditer(arr, flags=['refs_ok']): - if poly.item().size > 1: + for poly_ in np.nditer(arr, flags=['refs_ok']): + if poly_.item().size > 1: static = False break if not static: @@ -1283,8 +1283,8 @@ def _isstatic(self): that is, if the system has no dynamics. """ for list_of_polys in self.num, self.den: for row in list_of_polys: - for poly in row: - if len(poly) > 1: + for poly_ in row: + if len(poly_) > 1: return False return True From 71ea2d7df7951a5004a0ed4dd177bb3d61a1891d Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:49:48 +0200 Subject: [PATCH 12/22] Fix various ruff unknown symbol warnings --- control/freqplot.py | 2 +- control/nichols.py | 2 +- control/optimal.py | 4 ++-- control/phaseplot.py | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 097a05317..db06da0c1 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -2219,7 +2219,7 @@ def gangof4_plot( See :class:`ControlPlot` for more detailed information. """ - if len(args) == 1 and isinstance(arg, FrequencyResponseData): + if len(args) == 1 and isinstance(args[0], FrequencyResponseData): if any([kw is not None for kw in [omega, omega_limits, omega_num, Hz]]): raise ValueError( diff --git a/control/nichols.py b/control/nichols.py index b24482045..933bf7507 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -136,7 +136,7 @@ def nichols_plot( # Decide on the system name and label sysname = response.sysname if response.sysname is not None \ - else f"Unknown-{idx_sys}" + else f"Unknown-sys_{idx}" label_ = sysname if label is None else label[idx] # Generate the plot diff --git a/control/optimal.py b/control/optimal.py index dad1a9feb..10384fce0 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -162,7 +162,7 @@ def __init__( if trajectory_method is None: trajectory_method = 'collocation' if sys.isctime() else 'shooting' elif trajectory_method not in _optimal_trajectory_methods: - raise NotImplementedError(f"Unkown method {method}") + raise NotImplementedError(f"Unknown method {trajectory_method}") self.shooting = trajectory_method in {'shooting'} self.collocation = trajectory_method in {'collocation'} @@ -1105,7 +1105,7 @@ def solve_ocp( # Process (legacy) method keyword if kwargs.get('method'): method = kwargs.pop('method') - if method not in optimal_methods: + if method not in _optimal_trajectory_methods: if kwargs.get('minimize_method'): raise ValueError("'minimize_method' specified more than once") warnings.warn( diff --git a/control/phaseplot.py b/control/phaseplot.py index 9dd2b8837..f73be2b72 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -38,6 +38,7 @@ from . import config from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \ _process_ax_keyword, _update_plot_title +from .exception import ControlArgument from .nlsys import NonlinearIOSystem, find_operating_point, \ input_output_response From d1ac450777f47cee12cc3cdc4e16713c98ef94f4 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 13:50:37 +0200 Subject: [PATCH 13/22] Change _RLSortRoots in rlocus.py to placate ruff --- control/rlocus.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index 2ed88972e..065fbc10d 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -452,20 +452,18 @@ def _RLSortRoots(roots): one branch to another.""" sorted = zeros_like(roots) - for n, row in enumerate(roots): - if n == 0: - sorted[n, :] = row - else: - # sort the current row by finding the element with the - # smallest absolute distance to each root in the - # previous row - available = list(range(len(prevrow))) - for elem in row: - evect = elem - prevrow[available] - ind1 = abs(evect).argmin() - ind = available.pop(ind1) - sorted[n, ind] = elem - prevrow = sorted[n, :] + sorted[0] = roots[0] + for n, row in enumerate(roots[1:], start=1): + # sort the current row by finding the element with the + # smallest absolute distance to each root in the + # previous row + prevrow = sorted[n-1] + available = list(range(len(prevrow))) + for elem in row: + evect = elem - prevrow[available] + ind1 = abs(evect).argmin() + ind = available.pop(ind1) + sorted[n, ind] = elem return sorted From d8b8b2db7326f41c666845ff4196e225c55993ac Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 14:01:51 +0200 Subject: [PATCH 14/22] Remove unused import --- control/margins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/control/margins.py b/control/margins.py index 019c866be..80edc12c9 100644 --- a/control/margins.py +++ b/control/margins.py @@ -52,7 +52,6 @@ import numpy as np import scipy as sp from . import xferfcn -from .lti import evalfr from .iosys import issiso from . import frdata from . import freqplot From a032506adaa05ab0b96fce2ed890e53da67b8215 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 14:02:01 +0200 Subject: [PATCH 15/22] Silence ruff warning in phaseplot.py --- control/phaseplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/phaseplot.py b/control/phaseplot.py index f73be2b72..f5621253e 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -630,7 +630,7 @@ def separatrices( case (stable_color, unstable_color) | [stable_color, unstable_color]: pass case single_color: - stable_color = unstable_color = color + stable_color = unstable_color = single_color # Make sure all keyword arguments were processed if _check_kwargs and kwargs: From 5421a63491eb742cb99a39d6fe7cad0345fbfecf Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 1 Feb 2025 14:06:55 +0200 Subject: [PATCH 16/22] Add ruff check Github Action workflow --- .github/workflows/ruff-check.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/ruff-check.yml diff --git a/.github/workflows/ruff-check.yml b/.github/workflows/ruff-check.yml new file mode 100644 index 000000000..e056204bf --- /dev/null +++ b/.github/workflows/ruff-check.yml @@ -0,0 +1,29 @@ +# run ruff check on library source +# TODO: extend to tests, examples, benchmarks + +name: ruff-check + +on: [push, pull_request] + +jobs: + ruff-check-linux: + # ruff *shouldn't* be sensitive to platform + runs-on: ubuntu-latest + + steps: + - name: Checkout python-control + uses: actions/checkout@v3 + + - name: Setup environment + uses: actions/setup-python@v4 + with: + python-version: 3.13 # todo: latest? + + - name: Install ruff + run: | + python -m pip install --upgrade pip + python -m pip install ruff + + - name: Run ruff check + run: | + ruff check From 32ba2276d51e351a09c9e736ff5c452c44c475a4 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 08:55:45 +0200 Subject: [PATCH 17/22] Update docstring_test hashes --- control/tests/docstrings_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index b1fce53e0..a570166df 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -30,9 +30,9 @@ function_docstring_hash = { control.append: '1bddbac0fe932755c85e9fb0bfb97d88', control.describing_function_plot: '95f894706b1d3eeb3b854934596af09f', - control.dlqe: '9db995ed95c2214ce97074b0616a3191', + control.dlqe: 'b5d3c71aa178c3dd716875890d5b9ad7', control.dlqr: '896cfa651dbbd80e417635904d13c9d6', - control.lqe: '567bf657538935173f2e50700ba87168', + control.lqe: '54760eff0f5e7a203fc759e0826224fa', control.lqr: 'a3e0a85f781fc9c0f69a4b7da4f0bd22', control.margin: 'f02b3034f5f1d44ce26f916cc3e51600', control.parallel: 'bfc470aef75dbb923f9c6fb8bf3c9b43', @@ -40,7 +40,7 @@ control.ss2tf: 'e779b8d70205bc1218cc2a4556a66e4b', control.tf2ss: '086a3692659b7321c2af126f79f4bc11', control.markov: 'a4199c54cb50f07c0163d3790739eafe', - control.gangof4: '0e52eb6cf7ce024f9a41f3ae3ebf04f7', + control.gangof4: 'f9673ae4c6d26c202060ed4b9ef54800', } # List of keywords that we can skip testing (special cases) From b4776c4c71c88760d6c572d3da88945d8507f6d0 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 10:18:03 +0200 Subject: [PATCH 18/22] Fix comment wording --- control/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/__init__.py b/control/__init__.py index eb4eb1312..3f78452fa 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -72,7 +72,7 @@ # don't warn about `import *` # ruff: noqa: F403 -# don't warn about unknown names; they come `import *` +# don't warn about unknown names; they come via `import *` # ruff: noqa: F405 # Input/output system modules From bd705f2fa43a25b891f5beae8590186db248e6e1 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 10:18:16 +0200 Subject: [PATCH 19/22] Remove pyflakes weirdness, use noqa instead --- control/exception.py | 9 +++------ control/statesp.py | 7 +++---- control/xferfcn.py | 8 ++++---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/control/exception.py b/control/exception.py index 79ecb6ef3..feaf1f0ae 100644 --- a/control/exception.py +++ b/control/exception.py @@ -71,8 +71,7 @@ def slycot_check(): global slycot_installed if slycot_installed is None: try: - import slycot - slycot # pyflakes + import slycot # noqa: F401 slycot_installed = True except: slycot_installed = False @@ -86,8 +85,7 @@ def pandas_check(): global pandas_installed if pandas_installed is None: try: - import pandas - pandas # pyflakes + import pandas # noqa: F401 pandas_installed = True except: pandas_installed = False @@ -100,8 +98,7 @@ def cvxopt_check(): global cvxopt_installed if cvxopt_installed is None: try: - import cvxopt - cvxopt # pyflakes + import cvxopt # noqa: F401 cvxopt_installed = True except: cvxopt_installed = False diff --git a/control/statesp.py b/control/statesp.py index 28e9fc95c..5c5b6519d 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -21,8 +21,9 @@ import numpy as np import scipy as sp import scipy.linalg -# todo: check override of built-in any -from numpy import any, array, asarray, concatenate, cos, delete, empty, \ +# array needed in eval() call +from numpy import array # noqa: F401 +from numpy import any, asarray, concatenate, cos, delete, empty, \ exp, eye, isinf, pad, sin, squeeze, zeros from numpy.linalg import LinAlgError, eigvals, matrix_rank, solve from numpy.random import rand, randn @@ -40,8 +41,6 @@ from .nlsys import InterconnectedSystem, NonlinearIOSystem import control -array # pyflakes - try: from slycot import ab13dd except ImportError: diff --git a/control/xferfcn.py b/control/xferfcn.py index 5864bf104..1f8686c29 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -22,9 +22,11 @@ import numpy as np import scipy as sp -from numpy import array, delete, empty, exp, finfo, float64, ndarray, \ +# float64 needed in eval() call +from numpy import float64 # noqa: F401 +from numpy import array, delete, empty, exp, finfo, ndarray, \ nonzero, ones, poly, polyadd, polymul, polyval, real, roots, sqrt, \ - squeeze, where, zeros + where, zeros from scipy.signal import TransferFunction as signalTransferFunction from scipy.signal import cont2discrete, tf2zpk, zpk2tf @@ -36,8 +38,6 @@ _process_subsys_index, common_timebase from .lti import LTI, _process_frequency_response -float64 # pyflakes - __all__ = ['TransferFunction', 'tf', 'zpk', 'ss2tf', 'tfdata'] From ce1346592b6395f79a4e3f8658325023803c43ca Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 10:22:03 +0200 Subject: [PATCH 20/22] Update dlqe, lqe hashes in docstrings_test.py --- control/tests/docstrings_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index 2bc108bd4..44963cd7c 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -30,9 +30,9 @@ function_docstring_hash = { control.append: '1bddbac0fe932755c85e9fb0bfb97d88', control.describing_function_plot: '95f894706b1d3eeb3b854934596af09f', - control.dlqe: 'f2e52e35692cf5ffe911684d41d284c9', + control.dlqe: 'e1e9479310e4e5a6f50f5459fb3d2dfb', control.dlqr: '56d7f3a452bc8d7a7256a52d9d1dcb37', - control.lqe: 'f0ba6cde8191cbc10f052096ffc3fcbb', + control.lqe: '0447235d11b685b9dfaf485dd01fdb9a', control.lqr: 'a3e0a85f781fc9c0f69a4b7da4f0bd22', control.margin: 'f02b3034f5f1d44ce26f916cc3e51600', control.parallel: 'bfc470aef75dbb923f9c6fb8bf3c9b43', From b333298b1d42c56c92787e3e039a20c8d17a7ab6 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 10:23:53 +0200 Subject: [PATCH 21/22] Remove import of unused ControlDimension --- control/statefbk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/control/statefbk.py b/control/statefbk.py index 87b12da82..5d635196a 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -46,8 +46,7 @@ from . import statesp from .config import _process_legacy_keyword -from .exception import ControlArgument, ControlDimension, \ - ControlSlycot +from .exception import ControlArgument, ControlSlycot from .iosys import _process_indices, _process_labels, isctime, isdtime from .lti import LTI from .mateqn import care, dare From 18d3d591729b02b33a0b26b4079ce09351284c69 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 2 Feb 2025 10:24:08 +0200 Subject: [PATCH 22/22] Import missing StateSpace --- control/stochsys.py | 1 + 1 file changed, 1 insertion(+) diff --git a/control/stochsys.py b/control/stochsys.py index 537a06b49..1dca7c448 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -27,6 +27,7 @@ _process_control_disturbance_indices) from .nlsys import NonlinearIOSystem from .mateqn import care, dare, _check_shape +from .statesp import StateSpace from .exception import ControlArgument, ControlNotImplemented from .config import _process_legacy_keyword