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 diff --git a/control/__init__.py b/control/__init__.py index 1aaaa42e8..3f78452fa 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 via `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 * 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..feaf1f0ae 100644 --- a/control/exception.py +++ b/control/exception.py @@ -71,7 +71,7 @@ def slycot_check(): global slycot_installed if slycot_installed is None: try: - import slycot + import slycot # noqa: F401 slycot_installed = True except: slycot_installed = False @@ -85,7 +85,7 @@ def pandas_check(): global pandas_installed if pandas_installed is None: try: - import pandas + import pandas # noqa: F401 pandas_installed = True except: pandas_installed = False @@ -98,7 +98,7 @@ def cvxopt_check(): global cvxopt_installed if cvxopt_installed is None: try: - import cvxopt + import cvxopt # noqa: F401 cvxopt_installed = True except: cvxopt_installed = False 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 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..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 @@ -245,7 +246,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..db06da0c1 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 @@ -760,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'): @@ -2222,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/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/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 diff --git a/control/mateqn.py b/control/mateqn.py index 52b69e2b0..9100f567c 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/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 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/nichols.py b/control/nichols.py index ac42c9c37..933bf7507 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 @@ -137,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/nlsys.py b/control/nlsys.py index 7683d3382..39d61c4ee 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 @@ -211,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)] @@ -243,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)] @@ -811,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]} <- " @@ -831,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]} <- " @@ -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) @@ -2537,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=}") @@ -2576,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 @@ -2639,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') @@ -2665,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): @@ -2742,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/optimal.py b/control/optimal.py index 77cfd370e..10384fce0 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 @@ -163,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'} @@ -1106,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 e6123bc0e..f5621253e 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -38,7 +38,7 @@ from . import config from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \ _process_ax_keyword, _update_plot_title -from .exception import ControlNotImplemented +from .exception import ControlArgument from .nlsys import NonlinearIOSystem, find_operating_point, \ input_output_response @@ -162,7 +162,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): @@ -631,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: @@ -1091,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 @@ -1144,10 +1143,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 @@ -1257,15 +1257,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/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..065fbc10d 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 @@ -455,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 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 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 diff --git a/control/statesp.py b/control/statesp.py index c0e8e7862..a5b5f9fd2 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -16,14 +16,15 @@ 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 -from numpy import any, array, asarray, concatenate, cos, delete, empty, \ - exp, eye, isinf, ones, pad, sin, squeeze, zeros +# 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 from scipy.signal import StateSpace as signalStateSpace @@ -34,9 +35,9 @@ from .exception import ControlDimension, 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 .mateqn import _check_shape from .nlsys import InterconnectedSystem, NonlinearIOSystem diff --git a/control/stochsys.py b/control/stochsys.py index 5aaa29415..1dca7c448 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -16,16 +16,18 @@ __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 from .exception import ControlArgument, ControlNotImplemented from .config import _process_legacy_keyword @@ -164,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, G.shape[1], G.shape[1], name="QN") @@ -288,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()) @@ -459,7 +463,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/tests/docstrings_test.py b/control/tests/docstrings_test.py index 4dbf52ee8..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', @@ -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) 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"): 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..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 angle, array, delete, empty, exp, finfo, float64, ndarray, \ - nonzero, ones, pi, poly, polyadd, polymul, polyval, real, roots, sqrt, \ - squeeze, where, zeros +# 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, \ + where, zeros from scipy.signal import TransferFunction as signalTransferFunction from scipy.signal import cont2discrete, tf2zpk, zpk2tf @@ -33,7 +35,7 @@ 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 __all__ = ['TransferFunction', 'tf', 'zpk', 'ss2tf', 'tfdata'] @@ -219,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: @@ -1281,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 @@ -1501,7 +1503,6 @@ def _convert_to_transfer_function( """ from .statesp import StateSpace - kwargs = {} if isinstance(sys, TransferFunction): return sys 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 +]