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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy