diff --git a/Pending b/Pending deleted file mode 100644 index a1b5bda09..000000000 --- a/Pending +++ /dev/null @@ -1,63 +0,0 @@ -List of Pending changes for control-python -RMM, 5 Sep 09 - -This file contains brief notes on features that need to be added to -the python control library. Mainly intended to keep track of "bigger -picture" things that need to be done. - ---> See src/matlab.py for a list of MATLAB functions that eventually need - to be implemented. - -OPEN BUGS - * matlab.step() doesn't handle systems with a pole at the origin (use lsim2) - * TF <-> SS transformations are buggy; see tests/convert_test.py - * hsvd returns different value than MATLAB (2010a); see modelsimp_test.py - * lsim doesn't work for StateSpace systems (signal.lsim2 bug??) - -Transfer code from Roberto Bucher's yottalab to python-control - acker - pole placement using Ackermann method - c2d - contimous to discrete time conversion - full_obs - full order observer - red_obs - reduced order observer - comp_form - state feedback controller+observer in compact form - comp_form_i - state feedback controller+observer+integ in compact form - dsimul - simulate discrete time systems - dstep - step response (plot) of discrete time systems - dimpulse - imoulse response (plot) of discrete time systems - bb_step - step response (plot) of continous time systems - sysctr - system+controller+observer+feedback - care - Solve Riccati equation for contimous time systems - dare - Solve Riccati equation for discrete time systems - dlqr - discrete linear quadratic regulator - minreal - minimal state space representation - -Transfer code from Ryan Krauss's control.py to python-control - * phase margin computations (as part of margin command) - * step reponse - * c2d, c2d_tustin (compare to Bucher version first) - -Examples and test cases - * Put together unit tests for all functions (after deciding on framework) - * Figure out how to import 'figure' command properly (version issue?) - * Figure out source of BadCoefficients warning messages (pvtol-lqr and others) - * tests/test_all.py should report on failed tests - * tests/freqresp.py needs to be converted to unit test - * Convert examples/test-{response,statefbk}.py to unit tests - -Root locus plot improvements - * Make sure that scipy.signal.lti objects still work - * Update calling syntax to be consistent with other plotting commands - -State space class fixes - * Implement pzmap for state space systems - -Basic functions to be added - * margin - compute gain and phase margin (no plot) - * lyap - solve Lyapunov equation (use SLICOT SB03MD.f) - * See http://www.slicot.org/shared/libindex.html for list of functions - ----- -Instructions for building python package - * python setup.py build - * python setup.py install - * python setup.py sdist diff --git a/control/bdalg.py b/control/bdalg.py index 7bfd327eb..024d95fba 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -73,7 +73,7 @@ def series(sys1, *sysn, **kwargs): Parameters ---------- - sys1, sys2, ..., sysn: scalar, array, or :class:`InputOutputSystem` + sys1, sys2, ..., sysn : scalar, array, or :class:`InputOutputSystem` I/O systems to combine. Returns @@ -145,7 +145,7 @@ def parallel(sys1, *sysn, **kwargs): Parameters ---------- - sys1, sys2, ..., sysn: scalar, array, or :class:`InputOutputSystem` + sys1, sys2, ..., sysn : scalar, array, or :class:`InputOutputSystem` I/O systems to combine. Returns @@ -213,7 +213,7 @@ def negate(sys, **kwargs): Parameters ---------- - sys: scalar, array, or :class:`InputOutputSystem` + sys : scalar, array, or :class:`InputOutputSystem` I/O systems to negate. Returns @@ -265,9 +265,9 @@ def feedback(sys1, sys2=1, sign=-1, **kwargs): Parameters ---------- - sys1, sys2: scalar, array, or :class:`InputOutputSystem` + sys1, sys2 : scalar, array, or :class:`InputOutputSystem` I/O systems to combine. - sign: scalar + sign : scalar The sign of feedback. `sign` = -1 indicates negative feedback, and `sign` = 1 indicates positive feedback. `sign` is an optional argument; it assumes a value of -1 if not specified. @@ -412,8 +412,8 @@ def connect(sys, Q, inputv, outputv): """Index-based interconnection of an LTI system. .. deprecated:: 0.10.0 - `connect` will be removed in a future version of python-control in - favor of `interconnect`, which works with named signals. + `connect` will be removed in a future version of python-control. + Use :func:`interconnect` instead, which works with named signals. The system `sys` is a system typically constructed with `append`, with multiple inputs and outputs. The inputs and outputs are connected @@ -465,7 +465,7 @@ def connect(sys, Q, inputv, outputv): """ # TODO: maintain `connect` for use in MATLAB submodule (?) - warn("`connect` is deprecated; use `interconnect`", DeprecationWarning) + warn("connect() is deprecated; use interconnect()", FutureWarning) inputv, outputv, Q = \ np.atleast_1d(inputv), np.atleast_1d(outputv), np.atleast_1d(Q) diff --git a/control/canonical.py b/control/canonical.py index 7d091b22f..a62044322 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -204,7 +204,7 @@ def similarity_transform(xsys, T, timescale=1, inverse=False): The matrix `T` defines the new set of coordinates z = T x. timescale : float, optional If present, also rescale the time unit to tau = timescale * t - inverse: boolean, optional + inverse : bool, optional If True (default), transform so z = T x. If False, transform so x = T z. @@ -397,21 +397,21 @@ def bdschur(a, condmax=None, sort=None): Parameters ---------- - a : (M, M) array_like - Real matrix to decompose - condmax : None or float, optional - If None (default), use 1/sqrt(eps), which is approximately 1e8 - sort : {None, 'continuous', 'discrete'} - Block sorting; see below. + a : (M, M) array_like + Real matrix to decompose + condmax : None or float, optional + If None (default), use 1/sqrt(eps), which is approximately 1e8 + sort : {None, 'continuous', 'discrete'} + Block sorting; see below. Returns ------- - amodal : (M, M) real ndarray - Block-diagonal Schur decomposition of `a` - tmodal : (M, M) real ndarray - Similarity transform relating `a` and `amodal` - blksizes : (N,) int ndarray - Array of Schur block sizes + amodal : (M, M) real ndarray + Block-diagonal Schur decomposition of `a` + tmodal : (M, M) real ndarray + Similarity transform relating `a` and `amodal` + blksizes : (N,) int ndarray + Array of Schur block sizes Notes ----- diff --git a/control/config.py b/control/config.py index 260c7dac6..c5a59250b 100644 --- a/control/config.py +++ b/control/config.py @@ -83,6 +83,13 @@ def set_defaults(module, **keywords): The set_defaults() function can be used to modify multiple parameter values for a module at the same time, using keyword arguments. + Parameters + ---------- + module : str + Name of the module for which the defaults are being given. + **keywords : keyword arguments + Parameter value assignments. + Examples -------- >>> ct.defaults['freqplot.number_of_samples'] @@ -355,7 +362,7 @@ def _process_legacy_keyword(kwargs, oldkey, newkey, newval): if kwargs.get(oldkey) is not None: warnings.warn( f"keyword '{oldkey}' is deprecated; use '{newkey}'", - DeprecationWarning) + FutureWarning) if newval is not None: raise ControlArgument( f"duplicate keywords '{oldkey}' and '{newkey}'") diff --git a/control/ctrlplot.py b/control/ctrlplot.py index bc5b2cb04..7f6a37af4 100644 --- a/control/ctrlplot.py +++ b/control/ctrlplot.py @@ -207,11 +207,12 @@ def suptitle( title, fig=None, frame='axes', **kwargs): """Add a centered title to a figure. - This function is deprecated. Use :func:`ControlPlot.set_plot_title`. + .. deprecated:: 0.10.1 + Use :func:`ControlPlot.set_plot_title`. """ warnings.warn( - "suptitle is deprecated; use cplt.set_plot_title", FutureWarning) + "suptitle() is deprecated; use cplt.set_plot_title()", FutureWarning) _update_plot_title( title, fig=fig, frame=frame, use_existing=False, **kwargs) @@ -220,6 +221,10 @@ def suptitle( def get_plot_axes(line_array): """Get a list of axes from an array of lines. + .. deprecated:: 0.10.1 + This function will be removed in a future version of python-control. + Use `cplt.axes` to obtain axes for an instance of :class:`ControlPlot`. + This function can be used to return the set of axes corresponding to the line array that is returned by `time_response_plot`. This is useful for generating an axes array that can be passed to @@ -242,7 +247,8 @@ def get_plot_axes(line_array): Only the first element of each array entry is used to determine the axes. """ - warnings.warn("get_plot_axes is deprecated; use cplt.axes", FutureWarning) + warnings.warn( + "get_plot_axes() is deprecated; use cplt.axes()", FutureWarning) _get_axes = np.vectorize(lambda lines: lines[0].axes) if isinstance(line_array, ControlPlot): return _get_axes(line_array.lines) @@ -268,6 +274,9 @@ def pole_zero_subplots( Scaling to apply to the subplots. fig : :class:`matplotlib.figure.Figure` Figure to use for creating subplots. + rcParams : dict + Override the default parameters used for generating plots. + Default is set up config.default['ctrlplot.rcParams']. Returns ------- diff --git a/control/ctrlutil.py b/control/ctrlutil.py index 6cd32593b..c16b8c35a 100644 --- a/control/ctrlutil.py +++ b/control/ctrlutil.py @@ -88,7 +88,8 @@ def unwrap(angle, period=2*math.pi): def issys(obj): """Deprecated function to check if an object is an LTI system. - Use isinstance(obj, ct.LTI) + .. deprecated:: 0.10.0 + Use isinstance(obj, ct.LTI) """ warnings.warn("issys() is deprecated; use isinstance(obj, ct.LTI)", diff --git a/control/delay.py b/control/delay.py index d22e44107..9a05675b0 100644 --- a/control/delay.py +++ b/control/delay.py @@ -57,10 +57,10 @@ def pade(T, n=1, numdeg=None): time delay n : positive integer degree of denominator of approximation - numdeg: integer, or None (the default) - If None, numerator degree equals denominator degree - If >= 0, specifies degree of numerator - If < 0, numerator degree is n+numdeg + numdeg : integer, or None (the default) + If numdeg is `None`, numerator degree equals denominator degree. + If numdeg >= 0, specifies degree of numerator. + If numdeg < 0, numerator degree is n+numdeg. Returns ------- diff --git a/control/descfcn.py b/control/descfcn.py index 4dce09250..e17582c3f 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -102,6 +102,10 @@ def describing_function( A : array_like The amplitude(s) at which the describing function should be calculated. + num_points : int, optional + Number of points to use in computing describing function (default = + 100). + zero_check : bool, optional If `True` (default) then `A` is zero, the function will be evaluated and checked to make sure it is zero. If not, a `TypeError` exception @@ -271,7 +275,7 @@ def __len__(self): # Compute the describing function response + intersections def describing_function_response( H, F, A, omega=None, refine=True, warn_nyquist=None, - plot=False, check_kwargs=True, **kwargs): + _check_kwargs=True, **kwargs): """Compute the describing function response of a system. This function uses describing function analysis to analyze a closed @@ -294,6 +298,10 @@ def describing_function_response( Set to True to turn on warnings generated by `nyquist_plot` or False to turn off warnings. If not set (or set to None), warnings are turned off if omega is specified, otherwise they are turned on. + refine : bool, optional + If `True`, :func:`scipy.optimize.minimize` to refine the estimate + of the intersection of the frequency response and the describing + function. Returns ------- @@ -328,7 +336,7 @@ def describing_function_response( # Start by drawing a Nyquist curve response = nyquist_response( H, omega, warn_encirclements=warn_nyquist, warn_nyquist=warn_nyquist, - check_kwargs=check_kwargs, **kwargs) + _check_kwargs=_check_kwargs, **kwargs) H_omega, H_vals = response.contour.imag, H(response.contour) # Compute the describing function @@ -420,6 +428,8 @@ def describing_function_plot( If True (default), refine the location of the intersection of the Nyquist curve for the linear system and the describing function to determine the intersection point + label : str or array_like of str, optional + If present, replace automatically generated label with the given label. point_label : str, optional Formatting string used to label intersection points on the Nyquist plot. Defaults to "%5.2g @ %-5.2g". Set to `None` to omit labels. @@ -429,6 +439,10 @@ def describing_function_plot( Otherwise, a new figure is created. title : str, optional Set the title of the plot. Defaults to plot type and system name(s). + warn_nyquist : bool, optional + Set to True to turn on warnings generated by `nyquist_plot` or False + to turn off warnings. If not set (or set to None), warnings are + turned off if omega is specified, otherwise they are turned on. **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional Additional keywords passed to `matplotlib` to specify line properties for Nyquist curve. diff --git a/control/dtime.py b/control/dtime.py index 9b91eabd3..39b207e02 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -82,7 +82,7 @@ def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None, Other Parameters ---------------- inputs : int, list of str or None, optional - Description of the system inputs. If not specified, the origional + Description of the system inputs. If not specified, the original system inputs are used. See :class:`InputOutputSystem` for more information. outputs : int, list of str or None, optional diff --git a/control/frdata.py b/control/frdata.py index 1b35c6b20..c5018babb 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -597,12 +597,13 @@ def freqresp(self, omega): Method has been given the more pythonic name :meth:`FrequencyResponseData.frequency_response`. Or use :func:`freqresp` in the MATLAB compatibility module. + """ warn("FrequencyResponseData.freqresp(omega) will be removed in a " "future release of python-control; use " "FrequencyResponseData.frequency_response(omega), or " "freqresp(sys, omega) in the MATLAB compatibility module " - "instead", DeprecationWarning) + "instead", FutureWarning) return self.frequency_response(omega) def feedback(self, other=1, sign=-1): diff --git a/control/freqplot.py b/control/freqplot.py index 798b6da58..d80ee5a8f 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -147,8 +147,9 @@ def bode_plot( figure with the correct number and shape of axes, a new figure is created. The shape of the array must match the shape of the plotted data. - freq_label: str, optional - Frequency label (defaults to "rad/sec" or "Hertz") + freq_label, magnitude_label, phase_label : str, optional + Labels to use for the frequency, magnitude, and phase axes. + Defaults are set by `config.defaults['freqplot.']`. grid : bool, optional If True, plot grid lines on gain and phase plots. Default is set by `config.defaults['freqplot.grid']`. @@ -170,8 +171,6 @@ def bode_plot( legend_loc : int or str, optional Include a legend in the given location. Default is 'center right', with no legend for a single response. Use False to suppress legend. - magnitude_label : str, optional - Label to use for magnitude axis. Defaults to "Magnitude". margins_method : str, optional Method to use in computing margins (see :func:`stability_margins`). omega_limits : array_like of two values @@ -183,21 +182,34 @@ def bode_plot( Number of samples to use for the frequeny range. Defaults to config.defaults['freqplot.number_of_samples']. Ignored if data is not a list of systems. - phase_label : str, optional - Label to use for phase axis. Defaults to "Phase [rad]". + overlay_inputs, overlay_outputs : bool, optional + If set to True, combine input and/or output signals onto a single + plot and use line colors, labels, and a legend to distinguish them. plot : bool, optional (legacy) If given, `bode_plot` returns the legacy return values of magnitude, phase, and frequency. If False, just return the values with no plot. + plot_magnitude, plot_phase : bool, optional + If set to `False`, don't plot the magnitude or phase, respectively. rcParams : dict Override the default parameters used for generating plots. Default is set by config.default['ctrlplot.rcParams']. + share_frequency, share_magnitude, share_phase : str or bool, optional + Determine whether and how axis limits are shared between the + indicated variables. Can be set set to 'row' to share across all + subplots in a row, 'col' to set across all subplots in a column, or + `False` to allow independent limits. show_legend : bool, optional Force legend to be shown if ``True`` or hidden if ``False``. If ``None``, then show legend when there is more than one line on an axis or ``legend_loc`` or ``legend_map`` has been specified. title : str, optional Set the title of the plot. Defaults to plot type and system name(s). + title_frame : str, optional + Set the frame of reference used to center the plot title. If set to + 'axes' (default), the horizontal position of the title will be + centered relative to the axes. If set to 'figure', it will be + centered with respect to the figure (faster execution). wrap_phase : bool or float If wrap_phase is `False` (default), then the phase will be unwrapped so that it is continuously increasing or decreasing. If wrap_phase is @@ -416,8 +428,8 @@ def bode_plot( if plot is not None: warnings.warn( - "`bode_plot` return values of mag, phase, omega is deprecated; " - "use frequency_response()", DeprecationWarning) + "bode_plot() return value of mag, phase, omega is deprecated; " + "use frequency_response()", FutureWarning) if plot is False: # Process the data to match what we were sent @@ -1143,9 +1155,9 @@ def plot(self, *args, **kwargs): def nyquist_response( - sysdata, omega=None, plot=None, omega_limits=None, omega_num=None, + sysdata, omega=None, omega_limits=None, omega_num=None, return_contour=False, warn_encirclements=True, warn_nyquist=True, - check_kwargs=True, **kwargs): + _check_kwargs=True, **kwargs): """Nyquist response for a system. Computes a Nyquist contour for the system over a (optional) frequency @@ -1221,8 +1233,7 @@ def nyquist_response( right of stable poles and the left of unstable poles. If a pole is exactly on the imaginary axis, the `indent_direction` parameter can be used to set the direction of indentation. Setting `indent_direction` - to `none` will turn off indentation. If `return_contour` is True, the - exact contour used for evaluation is returned. + to `none` will turn off indentation. 3. For those portions of the Nyquist plot in which the contour is indented to avoid poles, resuling in a scaling of the Nyquist plot, @@ -1260,7 +1271,7 @@ def nyquist_response( indent_points = config._get_param( 'nyquist', 'indent_points', kwargs, _nyquist_defaults, pop=True) - if check_kwargs and kwargs: + if _check_kwargs and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Convert the first argument to a list @@ -1613,9 +1624,8 @@ def nyquist_plot( config.defaults['freqplot.number_of_samples']. Ignored if data is not a list of systems. plot : bool, optional - (legacy) If given, `bode_plot` returns the legacy return values - of magnitude, phase, and frequency. If False, just return the - values with no plot. + (legacy) If given, `nyquist_plot` returns the legacy return values + of (counts, contours). If False, return the values with no plot. primary_style : [str, str], optional Linestyles for primary image of the Nyquist curve. The first element is used for unscaled portions of the Nyquist curve, @@ -1624,7 +1634,7 @@ def nyquist_plot( determined by config.defaults['nyquist.mirror_style']. rcParams : dict Override the default parameters used for generating plots. - Default is set by config.default['freqplot.rcParams']. + Default is set by config.default['ctrlplot.rcParams']. return_contour : bool, optional (legacy) If 'True', return the encirclement count and Nyquist contour used to generate the Nyquist plot. @@ -1641,6 +1651,11 @@ def nyquist_plot( 4 and can be set using config.defaults['nyquist.start_marker_size']. title : str, optional Set the title of the plot. Defaults to plot type and system name(s). + title_frame : str, optional + Set the frame of reference used to center the plot title. If set to + 'axes' (default), the horizontal position of the title will + centered relative to the axes. If set to 'figure', it will be + centered with respect to the figure (faster execution). warn_nyquist : bool, optional If set to 'False', turn off warnings about frequencies above Nyquist. warn_encirclements : bool, optional @@ -1770,15 +1785,15 @@ def _parse_linestyle(style_name, allow_false=False): warn_encirclements=kwargs.pop('warn_encirclements', True), warn_nyquist=kwargs.pop('warn_nyquist', True), indent_radius=kwargs.pop('indent_radius', None), - check_kwargs=False, **kwargs) + _check_kwargs=False, **kwargs) else: nyquist_responses = data # Legacy return value processing if plot is not None or return_contour is not None: warnings.warn( - "`nyquist_plot` return values of count[, contour] is deprecated; " - "use nyquist_response()", DeprecationWarning) + "nyquist_plot() return value of count[, contour] is deprecated; " + "use nyquist_response()", FutureWarning) # Extract out the values that we will eventually return counts = [response.count for response in nyquist_responses] @@ -2207,6 +2222,7 @@ def gangof4_plot( *args, omega=omega, omega_limits=omega_limits, omega_num=omega_num, Hz=Hz).plot(**kwargs) + # # Singular values plot # @@ -2340,6 +2356,8 @@ def singular_values_plot( The matplotlib axes to draw the figure on. If not specified and the current figure has a single axes, that axes is used. Otherwise, a new figure is created. + color : matplotlib color spec + Color to use for singular values (or None for matplotlib default). grid : bool If True, plot grid lines on gain and phase plots. Default is set by `config.defaults['freqplot.grid']`. @@ -2364,13 +2382,18 @@ def singular_values_plot( the values with no plot. rcParams : dict Override the default parameters used for generating plots. - Default is set up config.default['freqplot.rcParams']. + Default is set up config.default['ctrlplot.rcParams']. show_legend : bool, optional Force legend to be shown if ``True`` or hidden if ``False``. If ``None``, then show legend when there is more than one line on an axis or ``legend_loc`` or ``legend_map`` has been specified. title : str, optional Set the title of the plot. Defaults to plot type and system name(s). + title_frame : str, optional + Set the frame of reference used to center the plot title. If set to + 'axes' (default), the horizontal position of the title will + centered relative to the axes. If set to 'figure', it will be + centered with respect to the figure (faster execution). See Also -------- @@ -2429,7 +2452,7 @@ def singular_values_plot( if plot is not None: warnings.warn( "`singular_values_plot` return values of sigma, omega is " - "deprecated; use singular_values_response()", DeprecationWarning) + "deprecated; use singular_values_response()", FutureWarning) # Warn the user if we got past something that is not real-valued if any([not np.allclose(np.imag(response.fresp[:, 0, :]), 0) diff --git a/control/iosys.py b/control/iosys.py index d00dade65..9092b672b 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -56,7 +56,7 @@ class InputOutputSystem(object): Description of the system inputs. This can be given as an integer count or a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be of the - form `s[i]` (where `s` is given by the `input_prefix` parameter and + form 's[i]' (where 's' is given by the `input_prefix` parameter and has default value 'u'). If this parameter is not given or given as `None`, the relevant quantity will be determined when possible based on other information provided to functions using the system. @@ -421,7 +421,7 @@ def isctime(self, strict=False): ---------- sys : Named I/O system System to be checked - strict: bool, optional + strict : bool, optional If strict is True, make sure that timebase is not None. Default is False. """ @@ -436,7 +436,7 @@ def isdtime(self, strict=False): Parameters ---------- - strict: bool, optional + strict : bool, optional If strict is True, make sure that timebase is not None. Default is False. """ @@ -466,7 +466,7 @@ def issiso(sys, strict=False): ---------- sys : I/O or LTI system System to be checked - strict: bool (default = False) + strict : bool (default = False) If strict is True, do not treat scalars as SISO """ if isinstance(sys, (int, float, complex, np.number)) and not strict: @@ -484,7 +484,21 @@ def timebase(sys, strict=True): dt = timebase(sys) returns the timebase for a system 'sys'. If the strict option is - set to False, dt = True will be returned as 1. + set to `True`, dt = True will be returned as 1. + + Parameters + ---------- + sys : InputOutputSystem or float + System whose timebase is to be determined. + strict : bool, optional + Whether to implement strict checking. If set to `True` (default), + a float will always be returned (dt = `True` will be returned as 1). + + Returns + ------- + dt : timebase + Timebase for the system (0 = continuous time, `None` = unspecified). + """ # System needs to be either a constant or an I/O or LTI system if isinstance(sys, (int, float, complex, np.number)): @@ -493,9 +507,9 @@ def timebase(sys, strict=True): raise ValueError("Timebase not defined") # Return the sample time, with converstion to float if strict is false - if (sys.dt == None): + if sys.dt == None: return None - elif (strict): + elif strict: return float(sys.dt) return sys.dt @@ -506,12 +520,12 @@ def common_timebase(dt1, dt2): Parameters ---------- - dt1, dt2: number or system with a 'dt' attribute (e.g. TransferFunction + dt1, dt2 : number or system with a 'dt' attribute (e.g. TransferFunction or StateSpace system) Returns ------- - dt: number + dt : number The common timebase of dt1 and dt2, as specified in :ref:`conventions-ref`. @@ -560,7 +574,7 @@ def isdtime(sys=None, strict=False, dt=None): System to be checked. dt : None or number, optional Timebase to be checked. - strict: bool, default=False + strict : bool, default=False If strict is True, make sure that timebase is not None. """ @@ -592,7 +606,7 @@ def isctime(sys=None, dt=None, strict=False): System to be checked. dt : None or number, optional Timebase to be checked. - strict: bool (default = False) + strict : bool (default = False) If strict is True, make sure that timebase is not None. """ diff --git a/control/lti.py b/control/lti.py index 2d69f6b91..e9455aed5 100644 --- a/control/lti.py +++ b/control/lti.py @@ -210,12 +210,12 @@ def poles(sys): Parameters ---------- - sys: StateSpace or TransferFunction + sys : StateSpace or TransferFunction Linear system Returns ------- - poles: ndarray + poles : ndarray Array that contains the system's poles. See Also @@ -235,7 +235,7 @@ def zeros(sys): Parameters ---------- - sys: StateSpace or TransferFunction + sys : StateSpace or TransferFunction Linear system Returns @@ -330,7 +330,7 @@ def evalfr(sys, x, squeeze=None): Parameters ---------- - sys: StateSpace or TransferFunction + sys : StateSpace or TransferFunction Linear system x : complex scalar or 1D array_like Complex frequency(s) @@ -514,14 +514,25 @@ def frequency_response( # Alternative name (legacy) def freqresp(sys, omega): - """Legacy version of frequency_response.""" - warn("freqresp is deprecated; use frequency_response", DeprecationWarning) + """Legacy version of frequency_response. + + .. deprecated:: 0.9.0 + This function will be removed in a future version of python-control. + Use `frequency_response` instead. + + """ + warn("freqresp() is deprecated; use frequency_response()", FutureWarning) return frequency_response(sys, omega) def dcgain(sys): """Return the zero-frequency (or DC) gain of the given system. + Parameters + ---------- + sys : LTI + System for which the zero-frequency gain is computed. + Returns ------- gain : ndarray @@ -544,11 +555,11 @@ def bandwidth(sys, dbdrop=-3): Parameters ---------- - sys: StateSpace or TransferFunction - Linear system + sys : StateSpace or TransferFunction + Linear system for which the bandwidth should be computed. dbdrop : float, optional By how much the gain drop in dB (default = -3) that defines the - bandwidth. Should be a negative scalar + bandwidth. Should be a negative scalar. Returns ------- diff --git a/control/mateqn.py b/control/mateqn.py index 05b47ffae..b73abdfcc 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -341,7 +341,7 @@ def dlyap(A, Q, C=None, E=None, method=None): # def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, - A_s="A", B_s="B", Q_s="Q", R_s="R", S_s="S", E_s="E"): + _As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E"): """Solves the continuous-time algebraic Riccati equation. X, L, G = care(A, B, Q, R=None) solves @@ -375,6 +375,9 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, Set the method used for computing the result. Current methods are 'slycot' and 'scipy'. If set to None (default), try 'slycot' first and then 'scipy'. + stabilizing : bool, optional + If `method` is 'slycot', unstabilized eigenvalues will be returned + in the initial elements of `L`. Not supported for 'scipy'. Returns ------- @@ -404,10 +407,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, m = B.shape[1] # Check to make sure input matrices are the right shape and type - _check_shape(A_s, A, n, n, square=True) - _check_shape(B_s, B, n, m) - _check_shape(Q_s, Q, n, n, square=True, symmetric=True) - _check_shape(R_s, R, m, m, square=True, symmetric=True) + _check_shape(_As, A, n, n, square=True) + _check_shape(_Bs, B, n, m) + _check_shape(_Qs, Q, n, n, square=True, symmetric=True) + _check_shape(_Rs, R, m, m, square=True, symmetric=True) # Solve the standard algebraic Riccati equation if S is None and E is None: @@ -454,8 +457,8 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2) # Check to make sure input matrices are the right shape and type - _check_shape(E_s, E, n, n, square=True) - _check_shape(S_s, S, n, m) + _check_shape(_Es, E, n, n, square=True) + _check_shape(_Ss, S, n, m) # See if we should solve this using SciPy if method == 'scipy': @@ -494,7 +497,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, return _ssmatrix(X), L, _ssmatrix(G) def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, - A_s="A", B_s="B", Q_s="Q", R_s="R", S_s="S", E_s="E"): + _As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E"): """Solves the discrete-time algebraic Riccati equation. @@ -529,6 +532,9 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, Set the method used for computing the result. Current methods are 'slycot' and 'scipy'. If set to None (default), try 'slycot' first and then 'scipy'. + stabilizing : bool, optional + If `method` is 'slycot', unstabilized eigenvalues will be returned + in the initial elements of `L`. Not supported for 'scipy'. Returns ------- @@ -558,14 +564,14 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, m = B.shape[1] # Check to make sure input matrices are the right shape and type - _check_shape(A_s, A, n, n, square=True) - _check_shape(B_s, B, n, m) - _check_shape(Q_s, Q, n, n, square=True, symmetric=True) - _check_shape(R_s, R, m, m, square=True, symmetric=True) + _check_shape(_As, A, n, n, square=True) + _check_shape(_Bs, B, n, m) + _check_shape(_Qs, Q, n, n, square=True, symmetric=True) + _check_shape(_Rs, R, m, m, square=True, symmetric=True) if E is not None: - _check_shape(E_s, E, n, n, square=True) + _check_shape(_Es, E, n, n, square=True) if S is not None: - _check_shape(S_s, S, n, m) + _check_shape(_Ss, S, n, m) # Figure out how to solve the problem if method == 'scipy': diff --git a/control/modelsimp.py b/control/modelsimp.py index 62e210e99..d2eadc4c3 100644 --- a/control/modelsimp.py +++ b/control/modelsimp.py @@ -109,24 +109,25 @@ def hankel_singular_values(sys): def model_reduction(sys, ELIM, method='matchdc'): - """ + """Model reduction by state elimination. + Model reduction of `sys` by eliminating the states in `ELIM` using a given method. Parameters ---------- - sys: StateSpace - Original system to reduce - ELIM: array - Vector of states to eliminate - method: string - Method of removing states in `ELIM`: either ``'truncate'`` or - ``'matchdc'``. + sys : StateSpace + Original system to reduce. + ELIM : array + Vector of states to eliminate. + method : string + Method of removing states in `ELIM`: either 'truncate' or + 'matchdc'. Returns ------- - rsys: StateSpace - A reduced order model + rsys : StateSpace + A reduced order model. Raises ------ @@ -220,6 +221,7 @@ def model_reduction(sys, ELIM, method='matchdc'): def balanced_reduction(sys, orders, method='truncate', alpha=None): """Balanced reduced order model of sys of a given order. + States are eliminated based on Hankel singular value. If sys has unstable modes, they are removed, the balanced realization is done on the stable part, then @@ -231,14 +233,14 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): Parameters ---------- - sys: StateSpace - Original system to reduce - orders: integer or array of integer + sys : StateSpace + Original system to reduce. + orders : integer or array of integer Desired order of reduced order model (if a vector, returns a vector - of systems) - method: string - Method of removing states, either ``'truncate'`` or ``'matchdc'``. - alpha: float + of systems). + method : string + Method of removing states, either ``'truncate'`` or ``'matchdc'``.. + alpha : float Redefines the stability boundary for eigenvalues of the system matrix A. By default for continuous-time systems, alpha <= 0 defines the stability boundary for the real part of A's eigenvalues @@ -248,7 +250,7 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): Returns ------- - rsys: StateSpace + rsys : StateSpace A reduced order model or a list of reduced order models if orders is a list. @@ -343,7 +345,8 @@ def balanced_reduction(sys, orders, method='truncate', alpha=None): def minimal_realization(sys, tol=None, verbose=True): - ''' + """ Eliminate uncontrollable or unobservable states. + Eliminates uncontrollable or unobservable states in state-space models or cancelling pole-zero pairs in transfer functions. The output sysr has minimal order and the same response @@ -351,18 +354,19 @@ def minimal_realization(sys, tol=None, verbose=True): Parameters ---------- - sys: StateSpace or TransferFunction - Original system - tol: real - Tolerance - verbose: bool - Print results if True + sys : StateSpace or TransferFunction + Original system. + tol : real + Tolerance. + verbose : bool + Print results if True. Returns ------- - rsys: StateSpace or TransferFunction - Cleaned model - ''' + rsys : StateSpace or TransferFunction + Cleaned model. + + """ sysr = sys.minreal(tol) if verbose: print("{nstates} states have been removed from the model".format( @@ -371,7 +375,7 @@ def minimal_realization(sys, tol=None, verbose=True): def _block_hankel(Y, m, n): - """Create a block Hankel matrix from impulse response""" + """Create a block Hankel matrix from impulse response.""" q, p, _ = Y.shape YY = Y.transpose(0,2,1) # transpose for reshape diff --git a/control/nichols.py b/control/nichols.py index 188b5ec0c..ac42c9c37 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -88,6 +88,9 @@ def nichols_plot( legend_loc : int or str, optional Include a legend in the given location. Default is 'upper left', with no legend for a single response. Use False to suppress legend. + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. show_legend : bool, optional Force legend to be shown if ``True`` or hidden if ``False``. If ``None``, then show legend when there is more than one line on the @@ -201,7 +204,7 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted', ax=None, ` ax : matplotlib.axes.Axes, optional Axes to add grid to. If ``None``, use ``matplotlib.pyplot.gca()``. - label_cl_phases: bool, optional + label_cl_phases : bool, optional If True, closed-loop phase lines will be labelled. Returns diff --git a/control/nlsys.py b/control/nlsys.py index d6d3b1b76..accd24c0f 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -66,7 +66,7 @@ class NonlinearIOSystem(InputOutputSystem): Description of the system inputs. This can be given as an integer count or as a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be - of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + of the form 's[i]' (where 's' is one of 'u', 'y', or 'x'). If this parameter is not given or given as `None`, the relevant quantity will be determined when possible based on other information provided to functions using the system. @@ -92,9 +92,8 @@ class NonlinearIOSystem(InputOutputSystem): generic name is generated with a unique integer id. params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. + Parameter values for the system. Passed to the evaluation functions + for the system as default values, overriding internal defaults. See Also -------- @@ -1244,7 +1243,7 @@ def nlsys( Description of the system inputs. This can be given as an integer count or as a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be - of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If + of the form 's[i]' (where 's' is one of 'u', 'y', or 'x'). If this parameter is not given or given as `None`, the relevant quantity will be determined when possible based on other information provided to functions using the system. @@ -1270,9 +1269,8 @@ def nlsys( generic name is generated with a unique integer id. params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. + Parameter values for the system. Passed to the evaluation functions + for the system as default values, overriding internal defaults. Returns ------- @@ -1320,31 +1318,28 @@ def input_output_response( ---------- sys : NonlinearIOSystem or list of NonlinearIOSystem I/O system(s) for which input/output response is simulated. - T : array-like Time steps at which the input is defined; values must be evenly spaced. - U : array-like, list, or number, optional Input array giving input at each time `T` (default = 0). If a list is specified, each element in the list will be treated as a portion of the input and broadcast (if necessary) to match the time vector. - X0 : array-like, list, or number, optional Initial condition (default = 0). If a list is given, each element in the list will be flattened and stacked into the initial condition. If a smaller number of elements are given that the number of states in the system, the initial condition will be padded with zeros. - t_eval : array-list, optional List of times at which the time response should be computed. Defaults to ``T``. - return_x : bool, optional If True, return the state vector when assigning to a tuple (default = False). See :func:`forced_response` for more details. If True, return the values of the state at each time (default = False). - + params : dict, optional + Parameter values for the system. Passed to the evaluation functions + for the system as default values, overriding internal defaults. squeeze : bool, optional If True and if the system has a single output, return the system output as a 1D array rather than a 2D array. If False, return the @@ -1379,16 +1374,19 @@ def input_output_response( Other parameters ---------------- - solve_ivp_method : str, optional - Set the method used by :func:`scipy.integrate.solve_ivp`. Defaults - to 'RK45'. - solve_ivp_kwargs : dict, optional - Pass additional keywords to :func:`scipy.integrate.solve_ivp`. ignore_errors : bool, optional If ``False`` (default), errors during computation of the trajectory will raise a ``RuntimeError`` exception. If ``True``, do not raise an exception and instead set ``results.success`` to ``False`` and place an error message in ``results.message``. + solve_ivp_method : str, optional + Set the method used by :func:`scipy.integrate.solve_ivp`. Defaults + to 'RK45'. + solve_ivp_kwargs : dict, optional + Pass additional keywords to :func:`scipy.integrate.solve_ivp`. + transpose : bool, default=False + If True, transpose all input and output arrays (for backward + compatibility with MATLAB and :func:`scipy.signal.lsim`). Raises ------ @@ -1675,6 +1673,8 @@ def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None, Parameters ---------- + sys : NonlinearIOSystem + I/O system for which the equilibrium point is sought. x0 : list of initial state values Initial guess for the value of the state near the equilibrium point. u0 : list of input values, optional @@ -1935,7 +1935,7 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw): Parameters ---------- sys : InputOutputSystem - The system to be linearized + The system to be linearized. xeq : array The state at which the linearization will be evaluated (does not need to be an equilibrium state). @@ -1969,7 +1969,7 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw): Other Parameters ---------------- inputs : int, list of str or None, optional - Description of the system inputs. If not specified, the origional + Description of the system inputs. If not specified, the original system inputs are used. See :class:`InputOutputSystem` for more information. outputs : int, list of str or None, optional @@ -2101,7 +2101,7 @@ def interconnect( Description of the system inputs. This can be given as an integer count or as a list of strings that name the individual signals. If an integer count is specified, the names of the signal will be of the - form `s[i]` (where `s` is one of `u`, `y`, or `x`). If this parameter + form 's[i]' (where 's' is one of 'u', 'y', or 'x'). If this parameter is not given or given as `None`, the relevant quantity will be determined when possible based on other information provided to functions using the system. diff --git a/control/optimal.py b/control/optimal.py index ce80eccfc..0eb49c823 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -72,7 +72,7 @@ class OptimalControlProblem(): Method to use for carrying out the optimization. Currently supported methods are 'shooting' and 'collocation' (continuous time only). The default value is 'shooting' for discrete time systems and - 'collocation' for continuous time systems + 'collocation' for continuous time systems. initial_guess : (tuple of) 1D or 2D array_like Initial states and/or inputs to use as a guess for the optimal trajectory. For shooting methods, an array of inputs for each time @@ -732,6 +732,7 @@ def _simulate_states(self, x0, inputs): logging.debug("input =\n" + str(inputs)) # Simulate the system to get the state + # TODO: update to use response object; remove return_x _, _, states = ct.input_output_response( self.system, self.timepts, inputs, x0, return_x=True, solve_ivp_kwargs=self.solve_ivp_kwargs, t_eval=self.timepts) @@ -981,7 +982,7 @@ def solve_ocp( timepts : 1D array_like List of times at which the optimal input should be computed. - X0: array-like or number, optional + X0 : array-like or number, optional Initial condition (default = 0). cost : callable @@ -1022,6 +1023,16 @@ def solve_ocp( 1D input of shape (ninputs,) that will be broadcast by extension of the time axis. + basis : BasisFamily, optional + Use the given set of basis functions for the inputs instead of + setting the value of the input at each point in the timepts vector. + + trajectory_method : string, optional + Method to use for carrying out the optimization. Currently supported + methods are 'shooting' and 'collocation' (continuous time only). The + default value is 'shooting' for discrete time systems and + 'collocation' for continuous time systems. + log : bool, optional If `True`, turn on logging messages (using Python logging module). @@ -1061,6 +1072,11 @@ def solve_ocp( res.states : array Time evolution of the state vector (if return_states=True). + Other Parameters + ---------------- + minimize_method : str, optional + Set the method used by :func:`scipy.optimize.minimize`. + Notes ----- 1. For discrete time systems, the final value of the timepts vector @@ -1079,9 +1095,9 @@ def solve_ocp( """ # Process keyword arguments - if trajectory_constraints is None: - # Backwards compatibility - trajectory_constraints = kwargs.pop('constraints', []) + trajectory_constraints = config._process_legacy_keyword( + kwargs, 'constraints', 'trajectory_constraints', + trajectory_constraints) # Allow 'return_x` as a synonym for 'return_states' return_states = ct.config._get_param( @@ -1095,14 +1111,14 @@ def solve_ocp( raise ValueError("'minimize_method' specified more than once") warnings.warn( "'method' parameter is deprecated; assuming minimize_method", - DeprecationWarning) + FutureWarning) kwargs['minimize_method'] = method else: if kwargs.get('trajectory_method'): raise ValueError("'trajectory_method' specified more than once") warnings.warn( "'method' parameter is deprecated; assuming trajectory_method", - DeprecationWarning) + FutureWarning) kwargs['trajectory_method'] = method # Set up the optimal control problem @@ -1168,6 +1184,10 @@ def create_mpc_iosystem( inputs, outputs, states : int or list of str, optional Set the names of the inputs, outputs, and states, as described in :func:`~control.InputOutputSystem`. + log : bool, optional + If `True`, turn on logging messages (using Python logging module). + Use :py:func:`logging.basicConfig` to enable logging output + (e.g., to a file). name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. @@ -1936,12 +1956,12 @@ def solve_oep( I/O system for which the optimal input will be computed. timepts : 1D array_like List of times at which the optimal input should be computed. - Y, U: 2D array_like + Y, U : 2D array_like Values of the outputs and inputs at each time point. trajectory_cost : callable Function that returns the cost given the current state and input. Called as `cost(y, u, x0)`. - X0: 1D array_like, optional + X0 : 1D array_like, optional Mean value of the initial condition (defaults to 0). trajectory_constraints : list of tuples, optional List of constraints that should hold at each point in the time vector. diff --git a/control/phaseplot.py b/control/phaseplot.py index 859c60c6a..abc050ffe 100644 --- a/control/phaseplot.py +++ b/control/phaseplot.py @@ -119,6 +119,11 @@ def phase_plane_plot( Other parameters ---------------- + dir : str, optional + Direction to draw streamlines: 'forward' to flow forward in time + from the reference points, 'reverse' to flow backward in time, or + 'both' to flow both forward and backward. The amount of time to + simulate in each direction is given by the ``timedata`` argument. plot_streamlines : bool or dict, optional If `True` (default) then plot streamlines based on the pointdata and gridtype. If set to a dict, pass on the key-value pairs in @@ -135,6 +140,9 @@ def phase_plane_plot( If `True` (default) then plot separatrices starting from each equilibrium point. If set to a dict, pass on the key-value pairs in the dict as keywords to :func:`~control.phaseplot.separatrices`. + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. suppress_warnings : bool, optional If set to `True`, suppress warning messages in generating trajectories. title : str, optional @@ -172,7 +180,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): kwargs, plot_streamlines, gridspec=gridspec, gridtype=gridtype, ax=ax) out[0] += streamlines( - sys, pointdata, timedata, check_kwargs=False, + sys, pointdata, timedata, _check_kwargs=False, suppress_warnings=suppress_warnings, **kwargs_local) # Get rid of keyword arguments handled by streamlines @@ -188,7 +196,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): kwargs_local = _create_kwargs( kwargs, plot_separatrices, gridspec=gridspec, ax=ax) out[0] += separatrices( - sys, pointdata, check_kwargs=False, **kwargs_local) + sys, pointdata, _check_kwargs=False, **kwargs_local) # Get rid of keyword arguments handled by separatrices for kw in ['arrows', 'arrow_size', 'arrow_style', 'params']: @@ -198,7 +206,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): kwargs_local = _create_kwargs( kwargs, plot_vectorfield, gridspec=gridspec, ax=ax) out[1] = vectorfield( - sys, pointdata, check_kwargs=False, **kwargs_local) + sys, pointdata, _check_kwargs=False, **kwargs_local) # Get rid of keyword arguments handled by vectorfield for kw in ['color', 'params']: @@ -208,7 +216,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): kwargs_local = _create_kwargs( kwargs, plot_equilpoints, gridspec=gridspec, ax=ax) out[2] = equilpoints( - sys, pointdata, check_kwargs=False, **kwargs_local) + sys, pointdata, _check_kwargs=False, **kwargs_local) # Get rid of keyword arguments handled by equilpoints for kw in ['params']: @@ -231,7 +239,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs): def vectorfield( sys, pointdata, gridspec=None, ax=None, suppress_warnings=False, - check_kwargs=True, **kwargs): + _check_kwargs=True, **kwargs): """Plot a vector field in the phase plane. This function plots a vector field for a two-dimensional state @@ -274,6 +282,9 @@ def vectorfield( Other parameters ---------------- + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. suppress_warnings : bool, optional If set to `True`, suppress warning messages in generating trajectories. @@ -301,7 +312,7 @@ def vectorfield( color = _get_color(kwargs, ax=ax) # Make sure all keyword arguments were processed - if check_kwargs and kwargs: + if _check_kwargs and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Generate phase plane (quiver) data @@ -321,7 +332,7 @@ def vectorfield( def streamlines( sys, pointdata, timedata=1, gridspec=None, gridtype=None, dir=None, - ax=None, check_kwargs=True, suppress_warnings=False, **kwargs): + ax=None, _check_kwargs=True, suppress_warnings=False, **kwargs): """Plot stream lines in the phase plane. This function plots stream lines for a two-dimensional state space @@ -352,6 +363,11 @@ def streamlines( If gridtype is 'circlegrid', then `gridspec` is a 2-tuple specifying the radius and number of points around each point in the `pointdata` array. + dir : str, optional + Direction to draw streamlines: 'forward' to flow forward in time + from the reference points, 'reverse' to flow backward in time, or + 'both' to flow both forward and backward. The amount of time to + simulate in each direction is given by the ``timedata`` argument. params : dict or list, optional Parameters to pass to system. For an I/O system, `params` should be a dict of parameters and values. For a callable, `params` should be @@ -367,6 +383,9 @@ def streamlines( Other parameters ---------------- + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. suppress_warnings : bool, optional If set to `True`, suppress warning messages in generating trajectories. @@ -399,7 +418,7 @@ def streamlines( color = _get_color(kwargs, ax=ax) # Make sure all keyword arguments were processed - if check_kwargs and kwargs: + if _check_kwargs and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Create reverse time system, if needed @@ -433,7 +452,7 @@ def streamlines( def equilpoints( - sys, pointdata, gridspec=None, color='k', ax=None, check_kwargs=True, + sys, pointdata, gridspec=None, color='k', ax=None, _check_kwargs=True, **kwargs): """Plot equilibrium points in the phase plane. @@ -474,6 +493,12 @@ def equilpoints( ------- out : list of Line2D objects + Other parameters + ---------------- + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. + """ # Process keywords rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True) @@ -496,7 +521,7 @@ def equilpoints( points, _ = _make_points(pointdata, gridspec, 'meshgrid') # Make sure all keyword arguments were processed - if check_kwargs and kwargs: + if _check_kwargs and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Search for equilibrium points @@ -513,7 +538,7 @@ def equilpoints( def separatrices( sys, pointdata, timedata=None, gridspec=None, ax=None, - check_kwargs=True, suppress_warnings=False, **kwargs): + _check_kwargs=True, suppress_warnings=False, **kwargs): """Plot separatrices in the phase plane. This function plots separatrices for a two-dimensional state space @@ -563,6 +588,9 @@ def separatrices( Other parameters ---------------- + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. suppress_warnings : bool, optional If set to `True`, suppress warning messages in generating trajectories. @@ -606,7 +634,7 @@ def separatrices( stable_color = unstable_color = color # Make sure all keyword arguments were processed - if check_kwargs and kwargs: + if _check_kwargs and kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Create a "reverse time" system to use for simulation @@ -686,13 +714,13 @@ def boxgrid(xvals, yvals): Parameters ---------- - xvals, yvals: 1D array-like + xvals, yvals : 1D array-like Array of points defining the points on the lower and left edges of the box. Returns ------- - grid: 2D array + grid : 2D array Array with shape (p, 2) defining the points along the edges of the box, where p is the number of points around the edge. @@ -715,7 +743,7 @@ def meshgrid(xvals, yvals): Parameters ---------- - xvals, yvals: 1D array-like + xvals, yvals : 1D array-like Array of points defining the points on the lower and left edges of the box. @@ -985,6 +1013,9 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, """(legacy) Phase plot for 2D dynamical systems. + .. deprecated:: 0.10.1 + This function is deprecated; use :func:`phase_plane_plot` instead. + Produces a vector field or stream line plot for a planar system. This function has been replaced by the :func:`~control.phase_plane_map` and :func:`~control.phase_plane_plot` functions. @@ -1044,7 +1075,7 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, """ # Generate a deprecation warning warnings.warn( - "phase_plot is deprecated; use phase_plot_plot instead", + "phase_plot() is deprecated; use phase_plane_plot() instead", FutureWarning) # @@ -1243,14 +1274,18 @@ def phase_plot(odefun, X=None, Y=None, scale=1, X0=None, T=None, def box_grid(xlimp, ylimp): """box_grid generate list of points on edge of box + .. deprecated:: 0.10.0 + Use :func:`phaseplot.boxgrid` instead. + list = box_grid([xmin xmax xnum], [ymin ymax ynum]) generates a list of points that correspond to a uniform grid at the end of the box defined by the corners [xmin ymin] and [xmax ymax]. + """ # Generate a deprecation warning warnings.warn( - "box_grid is deprecated; use phaseplot.boxgrid instead", + "box_grid() is deprecated; use phaseplot.boxgrid() instead", FutureWarning) return boxgrid( diff --git a/control/pzmap.py b/control/pzmap.py index c248cf84a..7b0f8d096 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -142,7 +142,7 @@ def pole_zero_map(sysdata): Parameters ---------- - sys : LTI system (StateSpace or TransferFunction) + sysdata : LTI system (StateSpace or TransferFunction) Linear system for which poles and zeros are computed. Returns @@ -185,7 +185,7 @@ def pole_zero_plot( Parameters ---------- - sysdata : List of PoleZeroData objects or LTI systems + data : List of PoleZeroData objects or LTI systems List of pole/zero response data objects generated by pzmap_response() or rootlocus_response() that are to be plotted. If a list of systems is given, the poles and zeros of those systems will be plotted. @@ -251,6 +251,9 @@ def pole_zero_plot( Set the size of the markers used for poles and zeros. marker_width : int, optional Set the line width of the markers used for poles and zeros. + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. scaling : str or list, optional Set the type of axis scaling. Can be 'equal' (default), 'auto', or a list of the form [xmin, xmax, ymin, ymax]. @@ -313,8 +316,8 @@ def pole_zero_plot( # Legacy return value processing if plot is not None: warnings.warn( - "`pole_zero_plot` return values of poles, zeros is deprecated; " - "use pole_zero_map()", DeprecationWarning) + "pole_zero_plot() return value of poles, zeros is deprecated; " + "use pole_zero_map()", FutureWarning) # Extract out the values that we will eventually return poles = [response.poles for response in pzmap_responses] diff --git a/control/rlocus.py b/control/rlocus.py index 95fda3e9a..189c2ccd0 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -47,7 +47,7 @@ def root_locus_map(sysdata, gains=None): Parameters ---------- - sys : LTI system or list of LTI systems + sysdata : LTI system or list of LTI systems Linear input/output systems (SISO only, for now). gains : array_like, optional Gains to use in computing plot of closed-loop poles. If not given, @@ -208,8 +208,8 @@ def root_locus_plot( # if plot is not None: warnings.warn( - "`root_locus` return values of roots, gains is deprecated; " - "use root_locus_map()", DeprecationWarning) + "root_locus() return value of roots, gains is deprecated; " + "use root_locus_map()", FutureWarning) if plot is False: return responses.loci, responses.gains diff --git a/control/robust.py b/control/robust.py index d5e5540fb..f9283af48 100644 --- a/control/robust.py +++ b/control/robust.py @@ -52,13 +52,17 @@ def h2syn(P, nmeas, ncon): Parameters ---------- - P: partitioned lti plant (State-space sys) - nmeas: number of measurements (input to controller) - ncon: number of control inputs (output from controller) + P : StateSpace + Partitioned LTI plant (state-space system). + nmeas : int + Number of measurements (input to controller). + ncon : int + Number of control inputs (output from controller). Returns ------- - K: controller to stabilize P (State-space sys) + K : StateSpace + Controller to stabilize `P`. Raises ------ @@ -121,25 +125,32 @@ def h2syn(P, nmeas, ncon): def hinfsyn(P, nmeas, ncon): + # TODO: document significance of rcond """H_{inf} control synthesis for plant P. Parameters ---------- - P: partitioned lti plant - nmeas: number of measurements (input to controller) - ncon: number of control inputs (output from controller) + P : StateSpace + Partitioned LTI plant (state-space system). + nmeas : int + Number of measurements (input to controller). + ncon : int + Number of control inputs (output from controller). Returns ------- - K: controller to stabilize P (State-space sys) - CL: closed loop system (State-space sys) - gam: infinity norm of closed loop system - rcond: 4-vector, reciprocal condition estimates of: - 1: control transformation matrix - 2: measurement transformation matrix - 3: X-Riccati equation - 4: Y-Riccati equation - TODO: document significance of rcond + K : StateSpace + Controller to stabilize `P`. + CL : StateSpace + Closed loop system. + gam : float + Infinity norm of closed loop system. + rcond : list + 4-vector, reciprocal condition estimates of: + 1: control transformation matrix + 2: measurement transformation matrix + 3: X-Riccati equation + 4: Y-Riccati equation Raises ------ @@ -263,18 +274,18 @@ def augw(g, w1=None, w2=None, w3=None): Parameters ---------- - g: LTI object, ny-by-nu - Plant - w1: None, scalar, or k1-by-ny LTI object - Weighting on S - w2: None, scalar, or k2-by-nu LTI object - Weighting on KS - w3: None, scalar, or k3-by-ny LTI object - Weighting on T + g : LTI object, ny-by-nu + Plant. + w1 : None, scalar, or k1-by-ny LTI object + Weighting on S. + w2 : None, scalar, or k2-by-nu LTI object + Weighting on KS. + w3 : None, scalar, or k3-by-ny LTI object + Weighting on T. Returns ------- - p: StateSpace + p : StateSpace Plant augmented with weightings, suitable for submission to hinfsyn or h2syn. @@ -375,20 +386,20 @@ def mixsyn(g, w1=None, w2=None, w3=None): Parameters ---------- - g: LTI - The plant for which controller must be synthesized - w1: None, or scalar or k1-by-ny LTI - Weighting on S = (1+G*K)**-1 - w2: None, or scalar or k2-by-nu LTI - Weighting on K*S - w3: None, or scalar or k3-by-ny LTI - Weighting on T = G*K*(1+G*K)**-1; + g : LTI + The plant for which controller must be synthesized. + w1 : None, or scalar or k1-by-ny LTI + Weighting on S = (1+G*K)**-1. + w2 : None, or scalar or k2-by-nu LTI + Weighting on K*S. + w3 : None, or scalar or k3-by-ny LTI + Weighting on T = G*K*(1+G*K)**-1. Returns ------- - k: StateSpace - Synthesized controller; - cl: StateSpace + k : StateSpace + Synthesized controller. + cl : StateSpace Closed system mapping evaluation inputs to evaluation outputs. Let p be the augmented plant, with:: @@ -400,10 +411,10 @@ def mixsyn(g, w1=None, w2=None, w3=None): info: tuple gamma: scalar - H-infinity norm of cl + H-infinity norm of cl. rcond: array - Estimates of reciprocal condition numbers - computed during synthesis. See hinfsyn for details + Estimates of reciprocal condition numbers computed during + synthesis. See hinfsyn for details. If a weighting w is scalar, it will be replaced by I*w, where I is ny-by-ny for w1 and w3, and nu-by-nu for w2. @@ -411,6 +422,7 @@ def mixsyn(g, w1=None, w2=None, w3=None): See Also -------- hinfsyn, augw + """ nmeas = g.noutputs ncon = g.ninputs diff --git a/control/sisotool.py b/control/sisotool.py index a6b9d468b..f34b210c6 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -324,30 +324,30 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', ---------- plant : :class:`LTI` (:class:`TransferFunction` or :class:`StateSpace` system) The dynamical system to be controlled. - gain : string (optional) + gain : string, optional Which gain to vary by `deltaK`. Must be one of `'P'`, `'I'`, or `'D'` (proportional, integral, or derative). - sign : int (optional) + sign : int, optional The sign of deltaK gain perturbation. - input : string (optional) + input_signal : string, optional The input used for the step response; must be `'r'` (reference) or `'d'` (disturbance) (see figure above). - Kp0, Ki0, Kd0 : float (optional) + Kp0, Ki0, Kd0 : float, optional Initial values for proportional, integral, and derivative gains, respectively. - deltaK : float (optional) + deltaK : float, optional Perturbation value for gain specified by the `gain` keywoard. - tau : float (optional) + tau : float, optional The time constant associated with the pole in the continuous-time derivative term. This is required to make the derivative transfer function proper. - C_ff : float or :class:`LTI` system (optional) + C_ff : float or :class:`LTI` system, optional Feedforward controller. If :class:`LTI`, must have timebase that is compatible with plant. - derivative_in_feedback_path : bool (optional) + derivative_in_feedback_path : bool, optional Whether to place the derivative term in feedback transfer function `C_b` instead of the forward transfer function `C_f`. - plot : bool (optional) + plot : bool, optional Whether to create Sisotool interactive plot. Returns diff --git a/control/statefbk.py b/control/statefbk.py index a385516ee..16eeb36ee 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -438,7 +438,7 @@ def lqr(*args, **kwargs): raise TypeError("unrecognized keywords: ", str(kwargs)) # Compute the result (dimension and symmetry checking done in care()) - X, L, G = care(A, B, Q, R, N, None, method=method, S_s="N") + X, L, G = care(A, B, Q, R, N, None, method=method, _Ss="N") return G, X, L @@ -575,7 +575,7 @@ def dlqr(*args, **kwargs): raise TypeError("unrecognized keywords: ", str(kwargs)) # Compute the result (dimension and symmetry checking done in dare()) - S, E, K = dare(A, B, Q, R, N, method=method, S_s="N") + S, E, K = dare(A, B, Q, R, N, method=method, _Ss="N") return _ssmatrix(K), _ssmatrix(S), E @@ -716,10 +716,10 @@ def create_statefbk_iosystem( specified as either integer offsets or as estimator/system output signal names. If not specified, defaults to the system states. - inputs, outputs : str, or list of str, optional + inputs, outputs, states : str, or list of str, optional List of strings that name the individual signals of the transformed - system. If not given, the inputs and outputs are the same as the - original system. + system. If not given, the inputs, outputs, and states are the same + as the original system. name : string, optional System name. If unspecified, a generic name is generated diff --git a/control/statesp.py b/control/statesp.py index 717fc9a73..aa1c7221b 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -355,13 +355,13 @@ def __init__(self, *args, **kwargs): def _get_states(self): warn("The StateSpace `states` attribute will be deprecated in a " "future release. Use `nstates` instead.", - DeprecationWarning, stacklevel=2) + FutureWarning, stacklevel=2) return self.nstates def _set_states(self, value): warn("The StateSpace `states` attribute will be deprecated in a " "future release. Use `nstates` instead.", - DeprecationWarning, stacklevel=2) + FutureWarning, stacklevel=2) self.nstates = value #: Deprecated attribute; use :attr:`nstates` instead. @@ -906,7 +906,7 @@ def freqresp(self, omega): warn("StateSpace.freqresp(omega) will be removed in a " "future release of python-control; use " "sys.frequency_response(omega), or freqresp(sys, omega) in the " - "MATLAB compatibility module instead", DeprecationWarning) + "MATLAB compatibility module instead", FutureWarning) return self.frequency_response(omega) # Compute poles and zeros @@ -1576,6 +1576,10 @@ def ss(*args, **kwargs): name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + method : str, optional + Set the method used for computing the result. Current methods are + 'slycot' and 'scipy'. If set to None (default), try 'slycot' first + and then 'scipy' (SISO only). Returns ------- @@ -1614,8 +1618,8 @@ def ss(*args, **kwargs): if len(args) > 0 and (hasattr(args[0], '__call__') or args[0] is None) \ and not isinstance(args[0], (InputOutputSystem, LTI)): # Function as first (or second) argument => assume nonlinear IO system - warn("using ss to create nonlinear I/O systems is deprecated; " - "use nlsys()", DeprecationWarning) + warn("using ss() to create nonlinear I/O systems is deprecated; " + "use nlsys()", FutureWarning) return NonlinearIOSystem(*args, **kwargs) elif len(args) == 4 or len(args) == 5: @@ -1663,7 +1667,7 @@ def ss2io(*args, **kwargs): Create an :class:`~control.StateSpace` system with the given signal and system names. See :func:`~control.ss` for more details. """ - warn("ss2io is deprecated; use ss()", DeprecationWarning) + warn("ss2io() is deprecated; use ss()", FutureWarning) return StateSpace(*args, **kwargs) @@ -1739,7 +1743,7 @@ def tf2io(*args, **kwargs): (2, 2, 8) """ - warn("tf2io is deprecated; use tf2ss() or tf()", DeprecationWarning) + warn("tf2io() is deprecated; use tf2ss() or tf()", FutureWarning) return tf2ss(*args, **kwargs) @@ -1915,15 +1919,12 @@ def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs): Parameters ---------- - inputs : int, list of str, or None - Description of the system inputs. This can be given as an integer - count or as a list of strings that name the individual signals. If an - integer count is specified, the names of the signal will be of the - form `s[i]` (where `s` is one of `u`, `y`, or `x`). - outputs : int, list of str, or None - Description of the system outputs. Same format as `inputs`. - states : int, list of str, or None - Description of the system states. Same format as `inputs`. + states, outputs, inputs : int, list of str, or None + Description of the system states, outputs, and inputs. This can be + given as an integer count or as a list of strings that name the + individual signals. If an integer count is specified, the names of + the signal will be of the form 's[i]' (where 's' is one of 'x', + 'y', or 'u'). strictly_proper : bool, optional If set to 'True', returns a proper system (no direct term). dt : None, True or float, optional diff --git a/control/stochsys.py b/control/stochsys.py index fe11a4fb5..b31083f19 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -177,7 +177,7 @@ def lqe(*args, **kwargs): # Compute the result (dimension and symmetry checking done in care()) P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN, method=method, - B_s="C", Q_s="QN", R_s="RN", S_s="NN") + _Bs="C", _Qs="QN", _Rs="RN", _Ss="NN") return _ssmatrix(LT.T), _ssmatrix(P), E @@ -297,7 +297,7 @@ def dlqe(*args, **kwargs): # Compute the result (dimension and symmetry checking done in dare()) P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method, - B_s="C", Q_s="QN", R_s="RN", S_s="NN") + _Bs="C", _Qs="QN", _Rs="RN", _Ss="NN") return _ssmatrix(LT.T), _ssmatrix(P), E @@ -604,6 +604,21 @@ def white_noise(T, Q, dt=0): covariance Q at each point in time (without any scaling based on the sample time). + Parameters + ---------- + T : 1D array_like + Array of linearly spaced times. + Q : 2D array_like + Noise intensity matrix of dimension nxn. + dt : float, optional + If 0, generate continuous time noise signal, otherwise discrete time. + + Returns + ------- + V : array + Noise signal indexed as `V[i, j]` where `i` is the signal index and + `j` is the time index. + """ # Convert input arguments to arrays T = np.atleast_1d(T) diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index b9e26e8c0..5629f27f9 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -269,7 +269,7 @@ def test_feedback_args(self, tsys): def testConnect(self, tsys): sys = append(tsys.sys2, tsys.sys3) # two siso systems - with pytest.warns(DeprecationWarning, match="use `interconnect`"): + with pytest.warns(FutureWarning, match="use interconnect()"): # should not raise error connect(sys, [[1, 2], [2, -2]], [2], [1, 2]) connect(sys, [[1, 2], [2, 0]], [2], [1, 2]) diff --git a/control/tests/conftest.py b/control/tests/conftest.py index 2330e3818..004b96058 100644 --- a/control/tests/conftest.py +++ b/control/tests/conftest.py @@ -65,10 +65,10 @@ def legacy_plot_signature(): import warnings warnings.filterwarnings( 'ignore', message='passing systems .* is deprecated', - category=DeprecationWarning) + category=FutureWarning) warnings.filterwarnings( - 'ignore', message='.* return values of .* is deprecated', - category=DeprecationWarning) + 'ignore', message='.* return value of .* is deprecated', + category=FutureWarning) yield warnings.resetwarnings() diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py new file mode 100644 index 000000000..28bd0f38c --- /dev/null +++ b/control/tests/docstrings_test.py @@ -0,0 +1,307 @@ +# docstrings_test.py - test for undocumented arguments +# RMM, 28 Jul 2024 +# +# This unit test looks through all functions in the package and attempts to +# identify arguments that are not documented. It will check anything that +# is an explicitly listed argument, as well as attempt to find keyword +# arguments that are extracted using kwargs.pop(), config._get_param(), or +# config.use_legacy_defaults. + +import inspect +import warnings + +import pytest +import re + +import control +import control.flatsys + +# List of functions that we can skip testing (special cases) +function_skiplist = [ + control.ControlPlot.reshape, # needed for legacy interface + control.phase_plot, # legacy function + control.drss, # documention in rss +] + +# Checksums to use for checking whether a docstring has changed +function_docstring_hash = { + control.append: '48548c4c4e0083312b3ea9e56174b0b5', + control.describing_function_plot: '95f894706b1d3eeb3b854934596af09f', + control.dlqe: '9db995ed95c2214ce97074b0616a3191', + control.dlqr: '896cfa651dbbd80e417635904d13c9d6', + control.lqe: '567bf657538935173f2e50700ba87168', + control.lqr: 'a3e0a85f781fc9c0f69a4b7da4f0bd22', + control.frd: '099464bf2d14f25a8769ef951adf658b', + control.margin: 'f02b3034f5f1d44ce26f916cc3e51600', + control.parallel: '025c5195a34c57392223374b6244a8c4', + control.series: '9aede1459667738f05cf4fc46603a4f6', + control.ss: '1b9cfad5dbdf2f474cfdeadf5cb1ad80', + control.ss2tf: '48ff25d22d28e7b396e686dd5eb58831', + control.tf: '53a13f4a7f75a31c81800e10c88730ef', + control.tf2ss: '086a3692659b7321c2af126f79f4bc11', + control.markov: '753309de348132ef238e78ac756412c1', + control.gangof4: '0e52eb6cf7ce024f9a41f3ae3ebf04f7', +} + +# List of keywords that we can skip testing (special cases) +keyword_skiplist = { + control.input_output_response: ['method'], + control.nyquist_plot: ['color'], # separate check + control.optimal.solve_ocp: ['method', 'return_x'], # deprecated + control.sisotool: ['kvect'], # deprecated + control.nyquist_response: ['return_contour'], # deprecated + control.create_estimator_iosystem: ['state_labels'], # deprecated + control.bode_plot: ['sharex', 'sharey', 'margin_info'], # deprecated + control.eigensys_realization: ['arg'], # quasi-positional +} + +# Decide on the level of verbosity (use -rP when running pytest) +verbose = 1 + +@pytest.mark.parametrize("module, prefix", [ + (control, ""), (control.flatsys, "flatsys."), + (control.optimal, "optimal."), (control.phaseplot, "phaseplot.") +]) +def test_parameter_docs(module, prefix): + checked = set() # Keep track of functions we have checked + + # Look through every object in the package + if verbose > 1: + print(f"Checking module {module}") + for name, obj in inspect.getmembers(module): + # Skip anything that is outside of this module + if inspect.getmodule(obj) is not None and ( + not inspect.getmodule(obj).__name__.startswith('control') + or prefix != "" and inspect.getmodule(obj) != module): + # Skip anything that isn't part of the control package + continue + + if inspect.isclass(obj): + if verbose > 1: + print(f" Checking class {name}") + # Check member functions within the class + test_parameter_docs(obj, prefix + name + '.') + + if inspect.isfunction(obj): + # Skip anything that is inherited, hidden, deprecated, or checked + if inspect.isclass(module) and name not in module.__dict__ \ + or name.startswith('_') or obj in function_skiplist or \ + obj in checked: + continue + else: + checked.add(obj) + + # Get the docstring (skip w/ warning if there isn't one) + if verbose > 1: + print(f" Checking function {name}") + if obj.__doc__ is None: + warnings.warn( + f"{module.__name__}.{name} is missing docstring") + continue + else: + docstring = inspect.getdoc(obj) + source = inspect.getsource(obj) + + # Skip deprecated functions + if ".. deprecated::" in docstring: + if verbose > 1: + print(" [deprecated]") + continue + elif re.search(name + r"(\(\))? is deprecated", docstring) or \ + "function is deprecated" in docstring: + if verbose > 1: + print(" [deprecated, but not numpydoc compliant]") + elif verbose: + print(f" {name} deprecation is not numpydoc compliant") + warnings.warn(f"{name} deprecated, but not numpydoc compliant") + continue + + elif re.search(name + r"(\(\))? is deprecated", source): + if verbose: + print(f" {name} is deprecated, but not documented") + warnings.warn(f"{name} deprecated, but not documented") + continue + + # Get the signature for the function + sig = inspect.signature(obj) + + # Go through each parameter and make sure it is in the docstring + for argname, par in sig.parameters.items(): + + # Look for arguments that we can skip + if argname == 'self' or argname[0] == '_' or \ + obj in keyword_skiplist and argname in keyword_skiplist[obj]: + continue + + # Check for positional arguments + if par.kind == inspect.Parameter.VAR_POSITIONAL: + if obj in function_docstring_hash: + import hashlib + hash = hashlib.md5( + (docstring + source).encode('utf-8')).hexdigest() + if function_docstring_hash[obj] != hash: + pytest.fail( + f"source/docstring for {name}() modified; " + f"recheck docstring and update hash to " + f"{hash=}") + continue + + # Too complicated to check + if f"*{argname}" not in docstring: + if verbose: + print(f" {name} has positional arguments; " + "check manually") + warnings.warn( + f"{name} {argname} has positional arguments; " + "docstring not checked") + continue + + # Check for keyword arguments (then look at code for parsing) + elif par.kind == inspect.Parameter.VAR_KEYWORD: + # See if we documented the keyward argumnt directly + # if f"**{argname} :" in docstring: + # continue + + # Look for direct kwargs argument access + kwargnames = set() + for _, kwargname in re.findall( + argname + r"(\[|\.pop\(|\.get\()'([\w]+)'", + source): + if verbose > 2: + print(" Found direct keyword argument", + kwargname) + kwargnames.add(kwargname) + + # Look for kwargs accessed via _get_param + for kwargname in re.findall( + r"_get_param\(\s*'\w*',\s*'([\w]+)',\s*" + argname, + source): + if verbose > 2: + print(" Found config keyword argument", + {kwargname}) + kwargnames.add(kwargname) + + # Look for kwargs accessed via _process_legacy_keyword + for kwargname in re.findall( + r"_process_legacy_keyword\([\s]*" + argname + + r",[\s]*'[\w]+',[\s]*'([\w]+)'", source): + if verbose > 2: + print(" Found legacy keyword argument", + {kwargname}) + kwargnames.add(kwargname) + + for kwargname in kwargnames: + if obj in keyword_skiplist and \ + kwargname in keyword_skiplist[obj]: + continue + if verbose > 3: + print(f" Checking keyword argument {kwargname}") + _check_parameter_docs( + name, kwargname, inspect.getdoc(obj), + prefix=prefix) + + # Make sure this argument is documented properly in docstring + else: + if verbose > 3: + print(f" Checking argument {argname}") + _check_parameter_docs( + name, argname, docstring, prefix=prefix) + + +@pytest.mark.parametrize("module, prefix", [ + (control, ""), (control.flatsys, "flatsys."), + (control.optimal, "optimal."), (control.phaseplot, "phaseplot.") +]) +def test_deprecated_functions(module, prefix): + checked = set() # Keep track of functions we have checked + + # Look through every object in the package + for name, obj in inspect.getmembers(module): + # Skip anything that is outside of this module + if inspect.getmodule(obj) is not None and ( + not inspect.getmodule(obj).__name__.startswith('control') + or prefix != "" and inspect.getmodule(obj) != module): + # Skip anything that isn't part of the control package + continue + + if inspect.isclass(obj): + # Check member functions within the class + test_deprecated_functions(obj, prefix + name + '.') + + if inspect.isfunction(obj): + # Skip anything that is inherited, hidden, or checked + if inspect.isclass(module) and name not in module.__dict__ \ + or name[0] == '_' or obj in checked: + continue + else: + checked.add(obj) + + # Get the docstring (skip w/ warning if there isn't one) + if obj.__doc__ is None: + warnings.warn( + f"{module.__name__}.{name} is missing docstring") + continue + else: + docstring = inspect.getdoc(obj) + source = inspect.getsource(obj) + + # Look for functions marked as deprecated in doc string + if ".. deprecated::" in docstring: + # Make sure a FutureWarning is issued + if not re.search("FutureWarning", source): + pytest.fail( + f"{name} deprecated but does not issue FutureWarning") + else: + if re.search(name + r"(\(\))? is deprecated", docstring) or \ + re.search(name + r"(\(\))? is deprecated", source): + pytest.fail( + f"{name} deprecated but w/ non-standard docs/warnings") + assert name != 'ss2io' + + +# Utility function to check for an argument in a docstring +def _check_parameter_docs(funcname, argname, docstring, prefix=""): + funcname = prefix + funcname + + # Find the "Parameters" section of docstring, where we start searching + if not (match := re.search(r"\nParameters\n----", docstring)): + pytest.fail(f"{funcname} docstring missing Parameters section") + else: + start = match.start() + + # Find the "Returns" section of the docstring (to be skipped, if present) + match_returns = re.search(r"\nReturns\n----", docstring) + + # Find the "Other Parameters" section of the docstring, if present + match_other = re.search(r"\nOther Parameters\n----", docstring) + + # Remove the returns section from docstring, in case output arguments + # match input argument names (it happens...) + if match_other and match_returns: + docstring = docstring[start:match_returns.start()] + \ + docstring[match_other.start():] + else: + docstring = docstring[start:] + + # Look for the parameter name in the docstring + if match := re.search( + "\n" + r"((\w+|\.{3}), )*" + argname + r"(, (\w+|\.{3}))*:", + docstring): + # Found the string, but not in numpydoc form + if verbose: + print(f" {funcname}: {argname} docstring missing space") + warnings.warn(f"{funcname} '{argname}' docstring missing space") + + elif not (match := re.search( + "\n" + r"((\w+|\.{3}), )*" + argname + r"(, (\w+|\.{3}))* :", + docstring)): + if verbose: + print(f" {funcname}: {argname} not documented") + pytest.fail(f"{funcname} '{argname}' not documented") + + # Make sure there isn't another instance + second_match = re.search( + "\n" + r"((\w+|\.{3}), )*" + argname + r"(, (\w+|\.{3}))*[ ]*:", + docstring[match.end():]) + if second_match: + pytest.fail(f"{funcname} '{argname}' documented twice") diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index e50af3c92..bae0ec47b 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -454,7 +454,7 @@ def test_eval(self): def test_freqresp_deprecated(self): sys_tf = ct.tf([1], [1, 2, 1]) frd_tf = frd(sys_tf, np.logspace(-1, 1, 3)) - with pytest.warns(DeprecationWarning): + with pytest.warns(FutureWarning): frd_tf.freqresp(1.) def test_repr_str(self): diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 555adf332..3ff1a51c5 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -60,7 +60,7 @@ def test_freqresp_siso(ss_siso): ctrl.frequency_response(ss_siso, omega) -@pytest.mark.filterwarnings("ignore:freqresp is deprecated") +@pytest.mark.filterwarnings(r"ignore:freqresp\(\) is deprecated") @slycotonly def test_freqresp_mimo_legacy(ss_mimo): """Test MIMO frequency response calls""" @@ -112,7 +112,7 @@ def test_nyquist_basic(ss_siso): # Check known warnings happened as expected assert len(record) == 2 assert re.search("encirclements was a non-integer", str(record[0].message)) - assert re.search("return values .* deprecated", str(record[1].message)) + assert re.search("return value .* deprecated", str(record[1].message)) response = nyquist_response(tf_siso, omega=np.logspace(-1, 1, 10)) assert len(response.contour) == 10 diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index cf4e3dd43..dd30ea71e 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -77,7 +77,7 @@ def test_tf2io(self, tsys): # Create a transfer function from the state space system linsys = tsys.siso_linsys tfsys = ct.ss2tf(linsys) - with pytest.warns(DeprecationWarning, match="use tf2ss"): + with pytest.warns(FutureWarning, match="use tf2ss"): iosys = ct.tf2io(tfsys) # Verify correctness via simulation @@ -90,13 +90,13 @@ def test_tf2io(self, tsys): # Make sure that non-proper transfer functions generate an error tfsys = ct.tf('s') with pytest.raises(ValueError): - with pytest.warns(DeprecationWarning, match="use tf2ss"): + with pytest.warns(FutureWarning, match="use tf2ss"): iosys=ct.tf2io(tfsys) def test_ss2io(self, tsys): # Create an input/output system from the linear system linsys = tsys.siso_linsys - with pytest.warns(DeprecationWarning, match="use ss"): + with pytest.warns(FutureWarning, match="use ss"): iosys = ct.ss2io(linsys) np.testing.assert_allclose(linsys.A, iosys.A) np.testing.assert_allclose(linsys.B, iosys.B) @@ -104,7 +104,7 @@ def test_ss2io(self, tsys): np.testing.assert_allclose(linsys.D, iosys.D) # Try adding names to things - with pytest.warns(DeprecationWarning, match="use ss"): + with pytest.warns(FutureWarning, match="use ss"): iosys_named = ct.ss2io(linsys, inputs='u', outputs='y', states=['x1', 'x2'], name='iosys_named') assert iosys_named.find_input('u') == 0 @@ -1942,7 +1942,7 @@ def test_nonuniform_timepts(nstates, noutputs, ninputs): def test_ss_nonlinear(): """Test ss() for creating nonlinear systems""" - with pytest.warns(DeprecationWarning, match="use nlsys()"): + with pytest.warns(FutureWarning, match="use nlsys()"): secord = ct.ss(secord_update, secord_output, inputs='u', outputs='y', states = ['x1', 'x2'], name='secord') assert secord.name == 'secord' @@ -1963,12 +1963,12 @@ def test_ss_nonlinear(): np.testing.assert_almost_equal(ss_response.outputs, io_response.outputs) # Make sure that optional keywords are allowed - with pytest.warns(DeprecationWarning, match="use nlsys()"): + with pytest.warns(FutureWarning, match="use nlsys()"): secord = ct.ss(secord_update, secord_output, dt=True) assert ct.isdtime(secord) # Make sure that state space keywords are flagged - with pytest.warns(DeprecationWarning, match="use nlsys()"): + with pytest.warns(FutureWarning, match="use nlsys()"): with pytest.raises(TypeError, match="unrecognized keyword"): ct.ss(secord_update, remove_useless_states=True) diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index 2ba3d5df8..26ff16774 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -83,6 +83,7 @@ class tsystems: @pytest.mark.usefixtures("fixedseed") +@pytest.mark.filterwarnings("ignore::FutureWarning") class TestMatlab: """Test matlab style functions""" @@ -173,6 +174,7 @@ def testPZmap(self, siso, subsys, mplcleanup): # pzmap(siso.ss1); not implemented # pzmap(siso.ss2); not implemented pzmap(getattr(siso, subsys)) + # TODO: check to make sure a plot got generated pzmap(getattr(siso, subsys), plot=False) def testStep(self, siso): @@ -404,6 +406,7 @@ def testDcgain_mimo(self, mimo): def testBode(self, siso, mplcleanup): """Call bode()""" + # TODO: make sure plots are generated bode(siso.ss1) bode(siso.tf1) bode(siso.tf2) diff --git a/control/tests/modelsimp_test.py b/control/tests/modelsimp_test.py index b811b5ac7..616ef5f09 100644 --- a/control/tests/modelsimp_test.py +++ b/control/tests/modelsimp_test.py @@ -54,13 +54,13 @@ def testMarkovSignature(self): with pytest.raises(ControlArgument): H = markov() - # to many positional arguments + # too many positional arguments with pytest.raises(ControlArgument): H = markov(Y,U,m,1) with pytest.raises(ControlArgument): H = markov(response,m,1) - # to many positional arguments + # too many positional arguments with pytest.raises(ControlDimension): U2 = np.hstack([U,U]) H = markov(Y,U2,m) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 823d65732..0d6907b64 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -295,7 +295,7 @@ def test_nyquist_indent_dont(indentsys): with pytest.warns() as record: count, contour = ct.nyquist_response( indentsys, omega=[0, 0.2, 0.3, 0.4], indent_radius=.1007, - plot=False, return_contour=True) + return_contour=True) np.testing.assert_allclose(contour[0], .1007+0.j) # second value of omega_vector is larger than indent_radius: not indented assert np.all(contour.real[2:] == 0.) @@ -322,7 +322,7 @@ def test_nyquist_indent_do(indentsys): # Make sure that the command also works if called directly as _plot() plt.figure() - with pytest.warns(DeprecationWarning, match=".* use nyquist_response()"): + with pytest.warns(FutureWarning, match=".* use nyquist_response()"): count, contour = ct.nyquist_plot( indentsys, indent_radius=0.01, return_contour=True) assert _Z(indentsys) == count + _P(indentsys) @@ -439,9 +439,11 @@ def test_nyquist_legacy(): response = ct.nyquist_plot(sys) def test_discrete_nyquist(): + # TODO: add tests to make sure plots make sense + # Make sure we can handle discrete time systems with negative poles sys = ct.tf(1, [1, -0.1], dt=1) * ct.tf(1, [1, 0.1], dt=1) - ct.nyquist_response(sys, plot=False) + ct.nyquist_response(sys) # system with a pole at the origin sys = ct.zpk([1,], [.3, 0], 1, dt=True) diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index 04eb037ab..438732b84 100644 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -16,7 +16,7 @@ from control import TransferFunction, config, pzmap -@pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") +@pytest.mark.filterwarnings("ignore:.*return value.*:FutureWarning") @pytest.mark.parametrize("kwargs", [pytest.param(dict(), id="default"), pytest.param(dict(plot=False), id="plot=False"), @@ -53,7 +53,8 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): if kwargs.get('plot', None) is None: pzkwargs['plot'] = True # use to get legacy return values - P, Z = pzmap(T, **pzkwargs) + with pytest.warns(FutureWarning, match="return value .* is deprecated"): + P, Z = pzmap(T, **pzkwargs) np.testing.assert_allclose(P, Pref, rtol=1e-3) np.testing.assert_allclose(Z, Zref, rtol=1e-3) @@ -96,7 +97,7 @@ def test_polezerodata(): # Legacy return format for plot in [True, False]: - with pytest.warns(DeprecationWarning, match=".* values .* deprecated"): + with pytest.warns(FutureWarning, match=".* value .* deprecated"): poles, zeros = ct.pole_zero_plot(pzdata, plot=False) np.testing.assert_equal(poles, sys.poles()) np.testing.assert_equal(zeros, sys.zeros()) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 38111e98e..a62bc742b 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -45,7 +45,7 @@ def check_cl_poles(self, sys, pole_list, k_list): poles = np.sort(poles) np.testing.assert_array_almost_equal(poles, poles_expected) - @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") + @pytest.mark.filterwarnings("ignore:.*return value.*:FutureWarning") def testRootLocus(self, sys): """Basic root locus (no plot)""" klist = [-1, 0, 1] @@ -61,7 +61,7 @@ def testRootLocus(self, sys): np.testing.assert_allclose(klist, k_out) self.check_cl_poles(sys, roots, klist) - @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") + @pytest.mark.filterwarnings("ignore:.*return value.*:FutureWarning") def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) @@ -109,7 +109,7 @@ def test_root_locus_plot_grid(self, sys, grid, method): # TODO: check validity of grid - @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") + @pytest.mark.filterwarnings("ignore:.*return value.*:FutureWarning") def test_root_locus_neg_false_gain_nonproper(self): """ Non proper TranferFunction with negative gain: Not implemented""" with pytest.raises(ValueError, match="with equal order"): @@ -147,7 +147,7 @@ def test_root_locus_zoom(self): assert_array_almost_equal(zoom_x, zoom_x_valid) assert_array_almost_equal(zoom_y, zoom_y_valid) - @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") + @pytest.mark.filterwarnings("ignore:.*return value.*:FutureWarning") @pytest.mark.timeout(2) def test_rlocus_default_wn(self): """Check that default wn calculation works properly""" @@ -186,7 +186,7 @@ def test_root_locus_plots(sys, grid, xlim, ylim, interactive): @pytest.mark.usefixtures("mplcleanup") def test_root_locus_legacy(keyword): sys = ct.rss(2, 1, 1) - with pytest.warns(DeprecationWarning, match=f"'{keyword}' is deprecated"): + with pytest.warns(FutureWarning, match=f"'{keyword}' is deprecated"): ct.root_locus_plot(sys, **{keyword: [0, 1, 2]}) diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index 146ae9e41..dde2423be 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -48,6 +48,7 @@ def testH2syn(self): np.testing.assert_array_almost_equal(k.D, [[0]]) +@pytest.mark.filterwarnings("ignore:connect:FutureWarning") class TestAugw: # tolerance for system equality @@ -324,6 +325,7 @@ def testErrors(self): augw(g1by1, w3=g2by2) +@pytest.mark.filterwarnings("ignore:connect:FutureWarning") class TestMixsyn: """Test control.robust.mixsyn""" diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 325b9c180..1fc744daa 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -153,6 +153,7 @@ def test_sisotool_initial_gain(self, tsys): with pytest.warns(FutureWarning): sisotool(tsys, kvect=1.2) + @pytest.mark.filterwarnings("ignore:connect:FutureWarning") def test_sisotool_mimo(self, sys222, sys221): # a 2x2 should not raise an error: sisotool(sys222) @@ -196,6 +197,7 @@ def test_pid_designer_1(self, plant, gain, sign, input_signal, Kp0, Ki0, Kd0, de {'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True}, {'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True}, {'input_signal':'r', 'Kd0':0.01, 'derivative_in_feedback_path':True}]) + @pytest.mark.filterwarnings("ignore:connect:FutureWarning") def test_pid_designer_2(self, plant, kwargs): rootlocus_pid_designer(plant, **kwargs) diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 4a0472de7..2d96ad225 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -892,7 +892,7 @@ def test_statefbk_errors(self): with pytest.raises(ControlArgument, match="gain must be an array"): ctrl, clsys = ct.create_statefbk_iosystem(sys, "bad argument") - with pytest.warns(DeprecationWarning, match="'type' is deprecated"): + with pytest.warns(FutureWarning, match="'type' is deprecated"): ctrl, clsys = ct.create_statefbk_iosystem(sys, K, type='nonlinear') with pytest.raises(ControlArgument, match="duplicate keywords"): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6ddf9933e..2829d6988 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -392,7 +392,7 @@ def test_freq_resp(self): np.testing.assert_almost_equal(omega, true_omega) # Deprecated version of the call (should return warning) - with pytest.warns(DeprecationWarning, match="will be removed"): + with pytest.warns(FutureWarning, match="will be removed"): mag, phase, omega = sys.freqresp(true_omega) np.testing.assert_almost_equal(mag, true_mag) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index cb5b38cba..14a11b669 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -487,7 +487,7 @@ def test_call_mimo(self): def test_freqresp_deprecated(self): sys = TransferFunction([1., 3., 5], [1., 6., 2., -1.]) # Deprecated version of the call (should generate warning) - with pytest.warns(DeprecationWarning): + with pytest.warns(FutureWarning): sys.freqresp(1.) def test_frequency_response_siso(self): diff --git a/control/timeplot.py b/control/timeplot.py index d29c212df..9f389b4ab 100644 --- a/control/timeplot.py +++ b/control/timeplot.py @@ -39,8 +39,8 @@ def time_response_plot( data, *fmt, ax=None, plot_inputs=None, plot_outputs=True, transpose=False, overlay_traces=False, overlay_signals=False, - legend=None, add_initial_zero=True, label=None, - trace_labels=None, title=None, relabel=True, **kwargs): + add_initial_zero=True, label=None, trace_labels=None, title=None, + relabel=True, **kwargs): """Plot the time response of an input/output system. This function creates a standard set of plots for the input/output @@ -126,8 +126,11 @@ def time_response_plot( output_props : array of dicts, optional List of line properties to use when plotting combined outputs. The default values are set by config.defaults['timeplot.output_props']. + rcParams : dict + Override the default parameters used for generating plots. + Default is set by config.default['ctrlplot.rcParams']. relabel : bool, optional - [deprecated] By default, existing figures and axes are relabeled + (deprecated) By default, existing figures and axes are relabeled when new data are added. If set to `False`, just plot new data on existing axes. show_legend : bool, optional diff --git a/control/xferfcn.py b/control/xferfcn.py index ba9af3913..ee41cbd2b 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -98,7 +98,7 @@ class TransferFunction(LTI): time, positive number is discrete time with specified sampling time, None indicates unspecified timebase (either continuous or discrete time). - display_format: None, 'poly' or 'zpk' + display_format : None, 'poly' or 'zpk', optional Set the display format used in printing the TransferFunction object. Default behavior is polynomial display and can be changed by changing config.defaults['xferfcn.display_format']. @@ -811,7 +811,7 @@ def __getitem__(self, key): num, den, self.dt, inputs=inputs, outputs=outputs, name=sysname) def freqresp(self, omega): - """(deprecated) Evaluate transfer function at complex frequencies. + """Evaluate transfer function at complex frequencies. .. deprecated::0.9.0 Method has been given the more pythonic name @@ -821,7 +821,7 @@ def freqresp(self, omega): warn("TransferFunction.freqresp(omega) will be removed in a " "future release of python-control; use " "sys.frequency_response(omega), or freqresp(sys, omega) in the " - "MATLAB compatibility module instead", DeprecationWarning) + "MATLAB compatibility module instead", FutureWarning) return self.frequency_response(omega) def poles(self): @@ -1578,13 +1578,13 @@ def tf(*args, **kwargs): Parameters ---------- - sys: LTI (StateSpace or TransferFunction) + sys : LTI (StateSpace or TransferFunction) A linear system - num: array_like, or list of list of array_like + num : array_like, or list of list of array_like Polynomial coefficients of the numerator - den: array_like, or list of list of array_like + den : array_like, or list of list of array_like Polynomial coefficients of the denominator - display_format: None, 'poly' or 'zpk' + display_format : None, 'poly' or 'zpk' Set the display format used in printing the TransferFunction object. Default behavior is polynomial display and can be changed by changing config.defaults['xferfcn.display_format'].. @@ -1677,7 +1677,7 @@ def tf(*args, **kwargs): raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) -def zpk(zeros, poles, gain, *args, **kwargs): +def zpk(zeros, poles, gain, dt=None, **kwargs): """zpk(zeros, poles, gain[, dt]) Create a transfer function from zeros, poles, gain. @@ -1711,7 +1711,7 @@ def zpk(zeros, poles, gain, *args, **kwargs): name : string, optional System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. - display_format: None, 'poly' or 'zpk' + display_format : None, 'poly' or 'zpk', optional Set the display format used in printing the TransferFunction object. Default behavior is polynomial display and can be changed by changing config.defaults['xferfcn.display_format']. @@ -1732,7 +1732,7 @@ def zpk(zeros, poles, gain, *args, **kwargs): """ num, den = zpk2tf(zeros, poles, gain) - return TransferFunction(num, den, *args, **kwargs) + return TransferFunction(num, den, dt=dt, **kwargs) def ss2tf(*args, **kwargs): @@ -1755,16 +1755,18 @@ def ss2tf(*args, **kwargs): Parameters ---------- - sys: StateSpace + sys : StateSpace A linear system - A: array_like or string + A : array_like or string System matrix - B: array_like or string + B : array_like or string Control matrix - C: array_like or string + C : array_like or string Output matrix - D: array_like or string + D : array_like or string Feedthrough matrix + **kwargs : keyword arguments + Additional arguments passed to :func:`tf` (e.g., signal names) Returns ------- @@ -1839,7 +1841,7 @@ def tfdata(sys): Parameters ---------- - sys: LTI (StateSpace, or TransferFunction) + sys : LTI (StateSpace, or TransferFunction) LTI system whose data will be returned Returns @@ -1859,7 +1861,7 @@ def _clean_part(data): Parameters ---------- - data: numerator or denominator of a transfer function. + data : numerator or denominator of a transfer function. Returns ------- diff --git a/doc/conf.py b/doc/conf.py index 824f57904..75981d630 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -103,7 +103,7 @@ # This config value contains the locations and names of other projects that # should be linked to in this documentation. intersphinx_mapping = \ - {'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), + {'scipy': ('https://docs.scipy.org/doc/scipy', None), 'numpy': ('https://numpy.org/doc/stable', None), 'matplotlib': ('https://matplotlib.org/stable/', None), } diff --git a/doc/plotting.rst b/doc/plotting.rst index 6832122af..167f5d001 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -654,11 +654,13 @@ Utility functions ----------------- These additional functions can be used to manipulate response data or -returned values from plotting routines. +carry out other operations in creating control plots. + .. autosummary:: :toctree: generated/ + ~control.box_grid ~control.combine_time_responses ~control.pole_zero_subplots ~control.reset_rcParams diff --git a/examples/steering-gainsched.py b/examples/steering-gainsched.py index 88eed9a95..85e8d8bda 100644 --- a/examples/steering-gainsched.py +++ b/examples/steering-gainsched.py @@ -222,7 +222,7 @@ def trajgen_output(t, x, u, params): vehicle, (gains, points), name='controller', ud_labels=['vd', 'phid'], gainsched_indices=['vd', 'theta'], gainsched_method='linear') -# Connect everything together (note that controller inputs are different +# Connect everything together (note that controller inputs are different) steering = ct.interconnect( # List of subsystems (trajgen, controller, vehicle), name='steering', @@ -235,7 +235,7 @@ def trajgen_output(t, x, u, params): ['controller.x', 'vehicle.x'], ['controller.y', 'vehicle.y'], ['controller.theta', 'vehicle.theta'], - ['controller.vd', ('trajgen', 'vd', 0.2)], # create error + ['controller.vd', ('trajgen', 'vd', 0.2)], # create some error ['controller.phid', 'trajgen.phid'], ['vehicle.v', 'controller.v'], ['vehicle.phi', 'controller.phi'] diff --git a/examples/vehicle-steering.png b/examples/vehicle-steering.png new file mode 100644 index 000000000..f10aab853 Binary files /dev/null and b/examples/vehicle-steering.png differ