diff --git a/control/descfcn.py b/control/descfcn.py index 14a345495..2ebb18569 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -26,7 +26,7 @@ # Class for nonlinearities with a built-in describing function class DescribingFunctionNonlinearity(): - """Base class for nonlinear systems with a describing function + """Base class for nonlinear systems with a describing function. This class is intended to be used as a base class for nonlinear functions that have an analytically defined describing function. Subclasses should @@ -36,16 +36,16 @@ class DescribingFunctionNonlinearity(): """ def __init__(self): - """Initailize a describing function nonlinearity (optional)""" + """Initailize a describing function nonlinearity (optional).""" pass def __call__(self, A): - """Evaluate the nonlinearity at a (scalar) input value""" + """Evaluate the nonlinearity at a (scalar) input value.""" raise NotImplementedError( "__call__() not implemented for this function (internal error)") def describing_function(self, A): - """Return the describing function for a nonlinearity + """Return the describing function for a nonlinearity. This method is used to allow analytical representations of the describing function for a nonlinearity. It turns the (complex) value @@ -56,7 +56,7 @@ def describing_function(self, A): "describing function not implemented for this function") def _isstatic(self): - """Return True if the function has no internal state (memoryless) + """Return True if the function has no internal state (memoryless). This internal function is used to optimize numerical computation of the describing function. It can be set to `True` if the instance @@ -329,7 +329,7 @@ def _find_intersection(L1a, L1b, L2a, L2b): # Saturation nonlinearity class saturation_nonlinearity(DescribingFunctionNonlinearity): - """Create a saturation nonlinearity for use in describing function analysis + """Create saturation nonlinearity for use in describing function analysis. This class creates a nonlinear function representing a saturation with given upper and lower bounds, including the describing function for the @@ -381,7 +381,7 @@ def describing_function(self, A): # Relay with hysteresis (FBS2e, Example 10.12) class relay_hysteresis_nonlinearity(DescribingFunctionNonlinearity): - """Relay w/ hysteresis nonlinearity for use in describing function analysis + """Relay w/ hysteresis nonlinearity for describing function analysis. This class creates a nonlinear function representing a a relay with symmetric upper and lower bounds of magnitude `b` and a hysteretic region @@ -437,7 +437,7 @@ def describing_function(self, A): # Friction-dominated backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) class friction_backlash_nonlinearity(DescribingFunctionNonlinearity): - """Backlash nonlinearity for use in describing function analysis + """Backlash nonlinearity for describing function analysis. This class creates a nonlinear function representing a friction-dominated backlash nonlinearity ,including the describing function for the diff --git a/control/flatsys/basis.py b/control/flatsys/basis.py index 7592b79a2..1ea957f52 100644 --- a/control/flatsys/basis.py +++ b/control/flatsys/basis.py @@ -47,6 +47,11 @@ class BasisFamily: :math:`z_i^{(q)}(t)` = basis.eval_deriv(self, i, j, t) + Parameters + ---------- + N : int + Order of the basis set. + """ def __init__(self, N): """Create a basis family of order N.""" diff --git a/control/flatsys/bezier.py b/control/flatsys/bezier.py index 5d0d551de..45a28995f 100644 --- a/control/flatsys/bezier.py +++ b/control/flatsys/bezier.py @@ -43,7 +43,7 @@ from .basis import BasisFamily class BezierFamily(BasisFamily): - r"""Polynomial basis functions. + r"""Bezier curve basis functions. This class represents the family of polynomials of the form diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py index 1905c4cb8..bbf1e7fc7 100644 --- a/control/flatsys/flatsys.py +++ b/control/flatsys/flatsys.py @@ -54,8 +54,59 @@ class FlatSystem(NonlinearIOSystem): """Base class for representing a differentially flat system. The FlatSystem class is used as a base class to describe differentially - flat systems for trajectory generation. The class must implement two - functions: + flat systems for trajectory generation. The output of the system does not + need to be the differentially flat output. + + Parameters + ---------- + forward : callable + A function to compute the flat flag given the states and input. + reverse : callable + A function to compute the states and input given the flat flag. + updfcn : callable, optional + Function returning the state update function + + `updfcn(t, x, u[, param]) -> array` + + where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array + with shape (ninputs,), `t` is a float representing the currrent + time, and `param` is an optional dict containing the values of + parameters used by the function. If not specified, the state + space update will be computed using the flat system coordinates. + outfcn : callable + Function returning the output at the given state + + `outfcn(t, x, u[, param]) -> array` + + where the arguments are the same as for `upfcn`. If not + specified, the output will be the flat outputs. + 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`). 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. + 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`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + Notes + ----- + The class must implement two functions: zflag = flatsys.foward(x, u) This function computes the flag (derivatives) of the flat output. @@ -83,65 +134,13 @@ def __init__(self, updfcn=None, outfcn=None, # I/O system inputs=None, outputs=None, states=None, params={}, dt=None, name=None): - """Create a differentially flat input/output system. + """Create a differentially flat I/O system. The FlatIOSystem constructor is used to create an input/output system - object that also represents a differentially flat system. The output - of the system does not need to be the differentially flat output. - - Parameters - ---------- - forward : callable - A function to compute the flat flag given the states and input. - reverse : callable - A function to compute the states and input given the flat flag. - updfcn : callable, optional - Function returning the state update function - - `updfcn(t, x, u[, param]) -> array` - - where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array - with shape (ninputs,), `t` is a float representing the currrent - time, and `param` is an optional dict containing the values of - parameters used by the function. If not specified, the state - space update will be computed using the flat system coordinates. - outfcn : callable - Function returning the output at the given state - - `outfcn(t, x, u[, param]) -> array` - - where the arguments are the same as for `upfcn`. If not - specified, the output will be the flat outputs. - 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`). 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. - 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`. - dt : None, True or float, optional - System timebase. None (default) indicates continuous - time, True indicates discrete time with undefined sampling - time, positive number is discrete time with specified - sampling time. - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. - name : string, optional - System name (used for specifying signals) - - Returns - ------- - InputOutputSystem - Input/output system object + object that also represents a differentially flat system. """ + # TODO: specify default update and output functions if updfcn is None: updfcn = self._flat_updfcn if outfcn is None: outfcn = self._flat_outfcn @@ -158,6 +157,7 @@ def __init__(self, # Save the length of the flat flag def forward(self, x, u, params={}): + """Compute the flat flag given the states and input. Given the states and inputs for a system, compute the flat diff --git a/control/flatsys/linflat.py b/control/flatsys/linflat.py index 6e74ed581..1e96a23d2 100644 --- a/control/flatsys/linflat.py +++ b/control/flatsys/linflat.py @@ -42,6 +42,41 @@ class LinearFlatSystem(FlatSystem, LinearIOSystem): + """Base class for a linear, differentially flat system. + + This class is used to create a differentially flat system representation + from a linear system. + + Parameters + ---------- + linsys : StateSpace + LTI StateSpace system to be converted + inputs : int, list of str or None, optional + 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 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. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. None (default) indicates continuous + time, True indicates discrete time with undefined sampling + time, positive number is discrete time with specified + sampling time. + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. + name : string, optional + System name (used for specifying signals) + + """ + def __init__(self, linsys, inputs=None, outputs=None, states=None, name=None): """Define a flat system from a SISO LTI system. @@ -49,39 +84,6 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, Given a reachable, single-input/single-output, linear time-invariant system, create a differentially flat system representation. - Parameters - ---------- - linsys : StateSpace - LTI StateSpace system to be converted - inputs : int, list of str or None, optional - 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 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. - outputs : int, list of str or None, optional - Description of the system outputs. Same format as `inputs`. - states : int, list of str, or None, optional - Description of the system states. Same format as `inputs`. - dt : None, True or float, optional - System timebase. None (default) indicates continuous - time, True indicates discrete time with undefined sampling - time, positive number is discrete time with specified - sampling time. - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. - name : string, optional - System name (used for specifying signals) - - Returns - ------- - iosys : LinearFlatSystem - Linear system represented as an flat input/output system - """ # Make sure we can handle the system if (not control.isctime(linsys)): diff --git a/control/flatsys/systraj.py b/control/flatsys/systraj.py index 4505d3563..c6ffb0867 100644 --- a/control/flatsys/systraj.py +++ b/control/flatsys/systraj.py @@ -41,30 +41,29 @@ class SystemTrajectory: """Class representing a system trajectory. - The `SystemTrajectory` class is used to represent the trajectory of - a (differentially flat) system. Used by the - :func:`~control.trajsys.point_to_point` function to return a - trajectory. + The `SystemTrajectory` class is used to represent the + trajectory of a (differentially flat) system. Used by the + :func:`~control.trajsys.point_to_point` function to return a trajectory. - """ - def __init__(self, sys, basis, coeffs=[], flaglen=[]): - """Initilize a system trajectory object. + Parameters + ---------- + sys : FlatSystem + Flat system object associated with this trajectory. + basis : BasisFamily + Family of basis vectors to use to represent the trajectory. + coeffs : list of 1D arrays, optional + For each flat output, define the coefficients of the basis + functions used to represent the trajectory. Defaults to an empty + list. + flaglen : list of ints, optional + For each flat output, the number of derivatives of the flat + output used to define the trajectory. Defaults to an empty + list. - Parameters - ---------- - sys : FlatSystem - Flat system object associated with this trajectory. - basis : BasisFamily - Family of basis vectors to use to represent the trajectory. - coeffs : list of 1D arrays, optional - For each flat output, define the coefficients of the basis - functions used to represent the trajectory. Defaults to an empty - list. - flaglen : list of ints, optional - For each flat output, the number of derivatives of the flat output - used to define the trajectory. Defaults to an empty list. + """ - """ + def __init__(self, sys, basis, coeffs=[], flaglen=[]): + """Initilize a system trajectory object.""" self.nstates = sys.nstates self.ninputs = sys.ninputs self.system = sys diff --git a/control/frdata.py b/control/frdata.py index c620984f6..5e2f3f2e1 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -57,25 +57,57 @@ class FrequencyResponseData(LTI): - """FrequencyResponseData(d, w) + """FrequencyResponseData(d, w[, smooth]) - A class for models defined by frequency response data (FRD) + A class for models defined by frequency response data (FRD). The FrequencyResponseData (FRD) class is used to represent systems in frequency response data form. - The main data members are 'omega' and 'fresp', where `omega` is a 1D array - with the frequency points of the response, and `fresp` is a 3D array, with - the first dimension corresponding to the output index of the FRD, the + Parameters + ---------- + d : 1D or 3D complex array_like + The frequency response at each frequency point. If 1D, the system is + assumed to be SISO. If 3D, the system is MIMO, with the first + dimension corresponding to the output index of the FRD, the second + dimension corresponding to the input index, and the 3rd dimension + corresponding to the frequency points in omega + w : iterable of real frequencies + List of frequency points for which data are available. + smooth : bool, optional + If ``True``, create an interpolation function that allows the + frequency response to be computed at any frequency within the range of + frequencies give in ``w``. If ``False`` (default), frequency response + can only be obtained at the frequencies specified in ``w``. + + Attributes + ---------- + ninputs, noutputs : int + Number of input and output variables. + omega : 1D array + Frequency points of the response. + fresp : 3D array + Frequency response, indexed by output index, input index, and + frequency point. + + Notes + ----- + The main data members are 'omega' and 'fresp', where 'omega' is a 1D array + of frequency points and and 'fresp' is a 3D array of frequency responses, + with the first dimension corresponding to the output index of the FRD, the second dimension corresponding to the input index, and the 3rd dimension corresponding to the frequency points in omega. For example, >>> frdata[2,5,:] = numpy.array([1., 0.8-0.2j, 0.2-0.8j]) - means that the frequency response from the 6th input to the 3rd - output at the frequencies defined in omega is set to the array - above, i.e. the rows represent the outputs and the columns - represent the inputs. + means that the frequency response from the 6th input to the 3rd output at + the frequencies defined in omega is set to the array above, i.e. the rows + represent the outputs and the columns represent the inputs. + + A frequency response data object is callable and returns the value of the + transfer function evaluated at a point in the complex plane (must be on + the imaginary access). See :meth:`~control.FrequencyResponseData.__call__` + for a more detailed description. """ @@ -83,7 +115,24 @@ class FrequencyResponseData(LTI): # https://docs.scipy.org/doc/numpy/reference/arrays.classes.html __array_priority__ = 11 # override ndarray and matrix types - epsw = 1e-8 + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system inputs. + #: + #: :meta hide-value: + ninputs = 1 + + #: Number of system outputs. + #: + #: :meta hide-value: + noutputs = 1 + + _epsw = 1e-8 #: Bound for exact frequency match def __init__(self, *args, **kwargs): """Construct an FRD object. @@ -141,7 +190,8 @@ def __init__(self, *args, **kwargs): self.omega = args[0].omega self.fresp = args[0].fresp else: - raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) + raise ValueError( + "Needs 1 or 2 arguments; received %i." % len(args)) # create interpolation functions if smooth: @@ -378,7 +428,7 @@ def eval(self, omega, squeeze=None): then single-dimensional axes are removed. """ - omega_array = np.array(omega, ndmin=1) # array-like version of omega + omega_array = np.array(omega, ndmin=1) # array-like version of omega # Make sure that we are operating on a simple list if len(omega_array.shape) > 1: @@ -389,7 +439,7 @@ def eval(self, omega, squeeze=None): raise ValueError("FRD.eval can only accept real-valued omega") if self.ifunc is None: - elements = np.isin(self.omega, omega) # binary array + elements = np.isin(self.omega, omega) # binary array if sum(elements) < len(omega_array): raise ValueError( "not all frequencies omega are in frequency list of FRD " @@ -398,7 +448,7 @@ def eval(self, omega, squeeze=None): out = self.fresp[:, :, elements] else: out = empty((self.noutputs, self.ninputs, len(omega_array)), - dtype=complex) + dtype=complex) for i in range(self.noutputs): for j in range(self.ninputs): for k, w in enumerate(omega_array): @@ -417,6 +467,9 @@ def __call__(self, s, squeeze=None): To evaluate at a frequency omega in radians per second, enter ``s = omega * 1j`` or use ``sys.eval(omega)`` + For a frequency response data object, the argument must be an + imaginary number (since only the frequency response is defined). + Parameters ---------- s : complex scalar or 1D array_like @@ -444,6 +497,7 @@ def __call__(self, s, squeeze=None): If `s` is not purely imaginary, because :class:`FrequencyDomainData` systems are only defined at imaginary frequency values. + """ # Make sure that we are operating on a simple list if len(np.atleast_1d(s).shape) > 1: @@ -451,7 +505,7 @@ def __call__(self, s, squeeze=None): if any(abs(np.atleast_1d(s).real) > 0): raise ValueError("__call__: FRD systems can only accept " - "purely imaginary frequencies") + "purely imaginary frequencies") # need to preserve array or scalar status if hasattr(s, '__len__'): @@ -510,6 +564,7 @@ def feedback(self, other=1, sign=-1): # fixes this problem. # + FRD = FrequencyResponseData @@ -534,7 +589,7 @@ def _convert_to_FRD(sys, omega, inputs=1, outputs=1): if isinstance(sys, FRD): omega.sort() if len(omega) == len(sys.omega) and \ - (abs(omega - sys.omega) < FRD.epsw).all(): + (abs(omega - sys.omega) < FRD._epsw).all(): # frequencies match, and system was already frd; simply use return sys diff --git a/control/iosys.py b/control/iosys.py index 526da4cdb..08249a651 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -95,11 +95,10 @@ class for a set of subclasses that are used to implement specific Dictionary of signal names for the inputs, outputs and states and the index of the corresponding array dt : None, True or float - System timebase. 0 (default) indicates continuous - time, True indicates discrete time with unspecified sampling - time, positive number is discrete time with specified - sampling time, None indicates unspecified timebase (either - continuous or discrete time). + System timebase. 0 (default) indicates continuous time, True indicates + discrete time with unspecified sampling time, positive number is + discrete time with specified sampling time, None indicates unspecified + timebase (either continuous or discrete time). params : dict, optional Parameter values for the systems. Passed to the evaluation functions for the system as default values, overriding internal defaults. @@ -120,12 +119,12 @@ class for a set of subclasses that are used to implement specific """ - idCounter = 0 + _idCounter = 0 - def name_or_default(self, name=None): + def _name_or_default(self, name=None): if name is None: - name = "sys[{}]".format(InputOutputSystem.idCounter) - InputOutputSystem.idCounter += 1 + name = "sys[{}]".format(InputOutputSystem._idCounter) + InputOutputSystem._idCounter += 1 return name def __init__(self, inputs=None, outputs=None, states=None, params={}, @@ -139,39 +138,6 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, :class:`~control.LinearIOSystem`, :class:`~control.NonlinearIOSystem`, :class:`~control.InterconnectedSystem`. - 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`). 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. - 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`. - dt : None, True or float, optional - System timebase. 0 (default) indicates continuous - time, True indicates discrete time with unspecified sampling - time, positive number is discrete time with specified - sampling time, None indicates unspecified timebase (either - continuous or discrete time). - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. - name : string, optional - System name (used for specifying signals). If unspecified, a - generic name is generated with a unique integer id. - - Returns - ------- - InputOutputSystem - Input/output system object - """ # Store the input arguments @@ -180,13 +146,35 @@ def __init__(self, inputs=None, outputs=None, states=None, params={}, # timebase self.dt = kwargs.get('dt', config.defaults['control.default_dt']) # system name - self.name = self.name_or_default(name) + self.name = self._name_or_default(name) # Parse and store the number of inputs, outputs, and states self.set_inputs(inputs) self.set_outputs(outputs) self.set_states(states) + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system inputs. + #: + #: :meta hide-value: + ninputs = 0 + + #: Number of system outputs. + #: + #: :meta hide-value: + noutputs = 0 + + #: Number of system states. + #: + #: :meta hide-value: + nstates = 0 + def __repr__(self): return self.name if self.name is not None else str(type(self)) @@ -665,7 +653,7 @@ def copy(self, newname=None): dup_prefix = config.defaults['iosys.duplicate_system_name_prefix'] dup_suffix = config.defaults['iosys.duplicate_system_name_suffix'] newsys = copy.copy(self) - newsys.name = self.name_or_default( + newsys.name = self._name_or_default( dup_prefix + self.name + dup_suffix if not newname else newname) return newsys @@ -676,6 +664,42 @@ class LinearIOSystem(InputOutputSystem, StateSpace): This class is used to implementat a system that is a linear state space system (defined by the StateSpace system object). + Parameters + ---------- + linsys : StateSpace + LTI StateSpace system to be converted + inputs : int, list of str or None, optional + 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 + 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. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous time, True indicates + discrete time with unspecified sampling time, positive number is + discrete time with specified sampling time, None indicates unspecified + timebase (either continuous or discrete time). + params : dict, optional + Parameter values for the systems. Passed to the evaluation functions + for the system as default values, overriding internal defaults. + name : string, optional + System name (used for specifying signals). If unspecified, a + generic name is generated with a unique integer id. + + Attributes + ---------- + ninputs, noutputs, nstates, dt, etc + See :class:`InputOutputSystem` for inherited attributes. + + A, B, C, D + See :class:`~control.StateSpace` for inherited attributes. + """ def __init__(self, linsys, inputs=None, outputs=None, states=None, name=None, **kwargs): @@ -683,42 +707,7 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, Converts a :class:`~control.StateSpace` system into an :class:`~control.InputOutputSystem` with the same inputs, outputs, and - states. The new system can be a continuous or discrete time system - - Parameters - ---------- - linsys : StateSpace - LTI StateSpace system to be converted - inputs : int, list of str or None, optional - 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 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. - outputs : int, list of str or None, optional - Description of the system outputs. Same format as `inputs`. - states : int, list of str, or None, optional - Description of the system states. Same format as `inputs`. - dt : None, True or float, optional - System timebase. 0 (default) indicates continuous - time, True indicates discrete time with unspecified sampling - time, positive number is discrete time with specified - sampling time, None indicates unspecified timebase (either - continuous or discrete time). - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. - name : string, optional - System name (used for specifying signals). If unspecified, a - generic name is generated with a unique integer id. - - Returns - ------- - iosys : LinearIOSystem - Linear system represented as an input/output system + states. The new system can be a continuous or discrete time system. """ if not isinstance(linsys, StateSpace): @@ -751,6 +740,17 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, if nstates is not None and linsys.nstates != nstates: raise ValueError("Wrong number/type of states given.") + # The following text needs to be replicated from StateSpace in order for + # this entry to show up properly in sphinx doccumentation (not sure why, + # but it was the only way to get it to work). + # + #: Deprecated attribute; use :attr:`nstates` instead. + #: + #: The ``state`` attribute was used to store the number of states for : a + #: state space system. It is no longer used. If you need to access the + #: number of states, use :attr:`nstates`. + states = property(StateSpace._get_states, StateSpace._set_states) + def _update_params(self, params={}, warning=True): # Parameters not supported; issue a warning if params and warning: @@ -772,78 +772,68 @@ def _out(self, t, x, u): class NonlinearIOSystem(InputOutputSystem): """Nonlinear I/O system. - This class is used to implement a system that is a nonlinear state - space system (defined by and update function and an output function). - - """ - def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, - states=None, params={}, name=None, **kwargs): - """Create a nonlinear I/O system given update and output functions. - - Creates an :class:`~control.InputOutputSystem` for a nonlinear system - by specifying a state update function and an output function. The new - system can be a continuous or discrete time system (Note: - discrete-time systems not yet supported by most function.) + Creates an :class:`~control.InputOutputSystem` for a nonlinear system by + specifying a state update function and an output function. The new system + can be a continuous or discrete time system (Note: discrete-time systems + are not yet supported by most functions.) - Parameters - ---------- - updfcn : callable - Function returning the state update function + Parameters + ---------- + updfcn : callable + Function returning the state update function - `updfcn(t, x, u[, param]) -> array` + `updfcn(t, x, u, params) -> array` - where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array - with shape (ninputs,), `t` is a float representing the currrent - time, and `param` is an optional dict containing the values of - parameters used by the function. + where `x` is a 1-D array with shape (nstates,), `u` is a 1-D array + with shape (ninputs,), `t` is a float representing the currrent + time, and `params` is a dict containing the values of parameters + used by the function. - outfcn : callable - Function returning the output at the given state + outfcn : callable + Function returning the output at the given state - `outfcn(t, x, u[, param]) -> array` + `outfcn(t, x, u, params) -> array` - where the arguments are the same as for `upfcn`. + where the arguments are the same as for `upfcn`. - inputs : int, list of str or None, optional - 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 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. - - outputs : int, list of str or None, optional - Description of the system outputs. Same format as `inputs`. + inputs : int, list of str or None, optional + 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 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. - states : int, list of str, or None, optional - Description of the system states. Same format as `inputs`. + outputs : int, list of str or None, optional + Description of the system outputs. Same format as `inputs`. - params : dict, optional - Parameter values for the systems. Passed to the evaluation - functions for the system as default values, overriding internal - defaults. + states : int, list of str, or None, optional + Description of the system states. Same format as `inputs`. - dt : timebase, optional - The timebase for the system, used to specify whether the system is - operating in continuous or discrete time. It can have the - following values: + params : dict, optional + Parameter values for the systems. Passed to the evaluation + functions for the system as default values, overriding internal + defaults. - * dt = 0: continuous time system (default) - * dt > 0: discrete time system with sampling period 'dt' - * dt = True: discrete time with unspecified sampling period - * dt = None: no timebase specified + dt : timebase, optional + The timebase for the system, used to specify whether the system is + operating in continuous or discrete time. It can have the + following values: - name : string, optional - System name (used for specifying signals). If unspecified, a - generic name is generated with a unique integer id. + * dt = 0: continuous time system (default) + * dt > 0: discrete time system with sampling period 'dt' + * dt = True: discrete time with unspecified sampling period + * dt = None: no timebase specified - Returns - ------- - iosys : NonlinearIOSystem - Nonlinear system represented as an input/output system. + name : string, optional + System name (used for specifying signals). If unspecified, a + generic name is generated with a unique integer id. - """ + """ + def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, + states=None, params={}, name=None, **kwargs): + """Create a nonlinear I/O system given update and output functions.""" # Look for 'input' and 'output' parameter name variants inputs = _parse_signal_parameter(inputs, 'input', kwargs) outputs = _parse_signal_parameter(outputs, 'output', kwargs) @@ -944,21 +934,14 @@ class InterconnectedSystem(InputOutputSystem): whose inputs and outputs are connected via a connection map. The overall system inputs and outputs are subsets of the subsystem inputs and outputs. + See :func:`~control.interconnect` for a list of parameters. + """ def __init__(self, syslist, connections=[], inplist=[], outlist=[], inputs=None, outputs=None, states=None, params={}, dt=None, name=None, **kwargs): - """Create an I/O system from a list of systems + connection info. + """Create an I/O system from a list of systems + connection info.""" - The InterconnectedSystem class is used to represent an input/output - system that consists of an interconnection between a set of subystems. - The outputs of each subsystem can be summed together to provide - inputs to other subsystems. The overall system inputs and outputs can - be any subset of subsystem inputs and outputs. - - See :func:`~control.interconnect` for a list of parameters. - - """ # Look for 'input' and 'output' parameter name variants inputs = _parse_signal_parameter(inputs, 'input', kwargs) outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) @@ -1425,6 +1408,9 @@ class LinearICSystem(InterconnectedSystem, LinearIOSystem): :class:`StateSpace` class structure, allowing it to be passed to functions that expect a :class:`StateSpace` system. + This class is usually generated using :func:`~control.interconnect` and + not called directly + """ def __init__(self, io_sys, ss_sys=None): @@ -1473,6 +1459,17 @@ def __init__(self, io_sys, ss_sys=None): else: raise TypeError("Second argument must be a state space system.") + # The following text needs to be replicated from StateSpace in order for + # this entry to show up properly in sphinx doccumentation (not sure why, + # but it was the only way to get it to work). + # + #: Deprecated attribute; use :attr:`nstates` instead. + #: + #: The ``state`` attribute was used to store the number of states for : a + #: state space system. It is no longer used. If you need to access the + #: number of states, use :attr:`nstates`. + states = property(StateSpace._get_states, StateSpace._set_states) + def input_output_response( sys, T, U=0., X0=0, params={}, @@ -2174,7 +2171,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], Notes ----- If a system is duplicated in the list of systems to be connected, - a warning is generated a copy of the system is created with the + a warning is generated and a copy of the system is created with the name of the new system determined by adding the prefix and suffix strings in config.defaults['iosys.linearized_system_name_prefix'] and config.defaults['iosys.linearized_system_name_suffix'], with the diff --git a/control/lti.py b/control/lti.py index 52f6b2e72..ef5d5569a 100644 --- a/control/lti.py +++ b/control/lti.py @@ -24,13 +24,13 @@ class LTI: """LTI is a parent class to linear time-invariant (LTI) system objects. - LTI is the parent to the StateSpace and TransferFunction child - classes. It contains the number of inputs and outputs, and the - timebase (dt) for the system. + LTI is the parent to the StateSpace and TransferFunction child classes. It + contains the number of inputs and outputs, and the timebase (dt) for the + system. This function is not generally called directly by the user. - The timebase for the system, dt, is used to specify whether the - system is operating in continuous or discrete time. It can have - the following values: + The timebase for the system, dt, is used to specify whether the system + is operating in continuous or discrete time. It can have the following + values: * dt = None No timebase specified * dt = 0 Continuous time system @@ -59,34 +59,52 @@ def __init__(self, inputs=1, outputs=1, dt=None): # future warning, so that users will see it. # - @property - def inputs(self): + def _get_inputs(self): warn("The LTI `inputs` attribute will be deprecated in a future " "release. Use `ninputs` instead.", DeprecationWarning, stacklevel=2) return self.ninputs - @inputs.setter - def inputs(self, value): + def _set_inputs(self, value): warn("The LTI `inputs` attribute will be deprecated in a future " "release. Use `ninputs` instead.", DeprecationWarning, stacklevel=2) self.ninputs = value - @property - def outputs(self): + #: Deprecated + inputs = property( + _get_inputs, _set_inputs, doc= + """ + Deprecated attribute; use :attr:`ninputs` instead. + + The ``input`` attribute was used to store the number of system inputs. + It is no longer used. If you need access to the number of inputs for + an LTI system, use :attr:`ninputs`. + """) + + def _get_outputs(self): warn("The LTI `outputs` attribute will be deprecated in a future " "release. Use `noutputs` instead.", DeprecationWarning, stacklevel=2) return self.noutputs - @outputs.setter - def outputs(self, value): + def _set_outputs(self, value): warn("The LTI `outputs` attribute will be deprecated in a future " "release. Use `noutputs` instead.", DeprecationWarning, stacklevel=2) self.noutputs = value + #: Deprecated + outputs = property( + _get_outputs, _set_outputs, doc= + """ + Deprecated attribute; use :attr:`noutputs` instead. + + The ``output`` attribute was used to store the number of system + outputs. It is no longer used. If you need access to the number of + outputs for an LTI system, use :attr:`noutputs`. + """) + def isdtime(self, strict=False): """ Check to see if a system is a discrete-time system diff --git a/control/optimal.py b/control/optimal.py index 63509ef4f..bbc8d0c9a 100644 --- a/control/optimal.py +++ b/control/optimal.py @@ -22,7 +22,7 @@ class OptimalControlProblem(): - """Description of a finite horizon, optimal control problem + """Description of a finite horizon, optimal control problem. The `OptimalControlProblem` class holds all of the information required to specify and optimal control problem: the system dynamics, cost function, @@ -31,12 +31,64 @@ class OptimalControlProblem(): `optimize.minimize` module, with the hope that this makes it easier to remember how to describe a problem. + Parameters + ---------- + sys : InputOutputSystem + 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. + integral_cost : callable + Function that returns the integral cost given the current state + and input. Called as integral_cost(x, u). + trajectory_constraints : list of tuples, optional + List of constraints that should hold at each point in the time + vector. Each element of the list should consist of a tuple with + first element given by :meth:`~scipy.optimize.LinearConstraint` or + :meth:`~scipy.optimize.NonlinearConstraint` and the remaining + elements of the tuple are the arguments that would be passed to + those functions. The constraints will be applied at each time + point along the trajectory. + terminal_cost : callable, optional + Function that returns the terminal cost given the current state + and input. Called as terminal_cost(x, u). + initial_guess : 1D or 2D array_like + Initial inputs to use as a guess for the optimal input. The + inputs should either be a 2D vector of shape (ninputs, horizon) + or a 1D input of shape (ninputs,) that will be broadcast by + extension of the time axis. + log : bool, optional + If `True`, turn on logging messages (using Python logging module). + kwargs : dict, optional + Additional parameters (passed to :func:`scipy.optimal.minimize`). + + Returns + ------- + ocp : OptimalControlProblem + Optimal control problem object, to be used in computing optimal + controllers. + + Additional parameters + --------------------- + solve_ivp_method : str, optional + Set the method used by :func:`scipy.integrate.solve_ivp`. + solve_ivp_kwargs : str, optional + Pass additional keywords to :func:`scipy.integrate.solve_ivp`. + minimize_method : str, optional + Set the method used by :func:`scipy.optimize.minimize`. + minimize_options : str, optional + Set the options keyword used by :func:`scipy.optimize.minimize`. + minimize_kwargs : str, optional + Pass additional keywords to :func:`scipy.optimize.minimize`. + Notes ----- - This class sets up an optimization over the inputs at each point in - time, using the integral and terminal costs as well as the - trajectory and terminal constraints. The `compute_trajectory` - method sets up an optimization problem that can be solved using + To describe an optimal control problem we need an input/output system, a + time horizon, a cost function, and (optionally) a set of constraints on + the state and/or input, either along the trajectory and at the terminal + time. This class sets up an optimization over the inputs at each point in + time, using the integral and terminal costs as well as the trajectory and + terminal constraints. The `compute_trajectory` method sets up an + optimization problem that can be solved using :func:`scipy.optimize.minimize`. The `_cost_function` method takes the information computes the cost of the @@ -62,63 +114,7 @@ def __init__( self, sys, timepts, integral_cost, trajectory_constraints=[], terminal_cost=None, terminal_constraints=[], initial_guess=None, basis=None, log=False, **kwargs): - """Set up an optimal control problem - - To describe an optimal control problem we need an input/output system, - a time horizon, a cost function, and (optionally) a set of constraints - on the state and/or input, either along the trajectory and at the - terminal time. - - Parameters - ---------- - sys : InputOutputSystem - 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. - integral_cost : callable - Function that returns the integral cost given the current state - and input. Called as integral_cost(x, u). - trajectory_constraints : list of tuples, optional - List of constraints that should hold at each point in the time - vector. Each element of the list should consist of a tuple with - first element given by :meth:`~scipy.optimize.LinearConstraint` or - :meth:`~scipy.optimize.NonlinearConstraint` and the remaining - elements of the tuple are the arguments that would be passed to - those functions. The constraints will be applied at each time - point along the trajectory. - terminal_cost : callable, optional - Function that returns the terminal cost given the current state - and input. Called as terminal_cost(x, u). - initial_guess : 1D or 2D array_like - Initial inputs to use as a guess for the optimal input. The - inputs should either be a 2D vector of shape (ninputs, horizon) - or a 1D input of shape (ninputs,) that will be broadcast by - extension of the time axis. - log : bool, optional - If `True`, turn on logging messages (using Python logging module). - kwargs : dict, optional - Additional parameters (passed to :func:`scipy.optimal.minimize`). - - Returns - ------- - ocp : OptimalControlProblem - Optimal control problem object, to be used in computing optimal - controllers. - - Additional parameters - --------------------- - solve_ivp_method : str, optional - Set the method used by :func:`scipy.integrate.solve_ivp`. - solve_ivp_kwargs : str, optional - Pass additional keywords to :func:`scipy.integrate.solve_ivp`. - minimize_method : str, optional - Set the method used by :func:`scipy.optimize.minimize`. - minimize_options : str, optional - Set the options keyword used by :func:`scipy.optimize.minimize`. - minimize_kwargs : str, optional - Pass additional keywords to :func:`scipy.optimize.minimize`. - - """ + """Set up an optimal control problem.""" # Save the basic information for use later self.system = sys self.timepts = timepts @@ -772,9 +768,9 @@ def compute_mpc(self, x, squeeze=None): # Optimal control result class OptimalControlResult(sp.optimize.OptimizeResult): - """Represents the optimal control result + """Result from solving an optimal control problem. - This class is a subclass of :class:`sp.optimize.OptimizeResult` with + This class is a subclass of :class:`scipy.optimize.OptimizeResult` with additional attributes associated with solving optimal control problems. Attributes diff --git a/control/statesp.py b/control/statesp.py index 92834b3e4..6b3a1dff3 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -160,23 +160,49 @@ def _f2s(f): class StateSpace(LTI): """StateSpace(A, B, C, D[, dt]) - A class for representing state-space models + A class for representing state-space models. The StateSpace class is used to represent state-space realizations of linear time-invariant (LTI) systems: dx/dt = A x + B u - y = C x + D u + y = C x + D u where u is the input, y is the output, and x is the state. - The main data members are the A, B, C, and D matrices. The class also - keeps track of the number of states (i.e., the size of A). The data - format used to store state space matrices is set using the value of - `config.defaults['use_numpy_matrix']`. If True (default), the state space - elements are stored as `numpy.matrix` objects; otherwise they are - `numpy.ndarray` objects. The :func:`~control.use_numpy_matrix` function - can be used to set the storage type. + Parameters + ---------- + A, B, C, D: array_like + System matrices of the appropriate dimensions. + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous + time, True indicates discrete time with unspecified sampling + time, positive number is discrete time with specified + sampling time, None indicates unspecified timebase (either + continuous or discrete time). + + Attributes + ---------- + ninputs, noutputs, nstates : int + Number of input, output and state variables. + A, B, C, D : 2D arrays + System matrices defining the input/output dynamics. + dt : None, True or float + System timebase. 0 (default) indicates continuous time, True indicates + discrete time with unspecified sampling time, positive number is + discrete time with specified sampling time, None indicates unspecified + timebase (either continuous or discrete time). + + Notes + ----- + The main data members in the ``StateSpace`` class are the A, B, C, and D + matrices. The class also keeps track of the number of states (i.e., + the size of A). The data format used to store state space matrices is + set using the value of `config.defaults['use_numpy_matrix']`. If True + (default), the state space elements are stored as `numpy.matrix` objects; + otherwise they are `numpy.ndarray` objects. The + :func:`~control.use_numpy_matrix` function can be used to set the storage + type. A discrete time system is created by specifying a nonzero 'timebase', dt when the system is constructed: @@ -195,6 +221,10 @@ class StateSpace(LTI): The default value of dt can be changed by changing the value of ``control.config.defaults['control.default_dt']``. + A state space system is callable and returns the value of the transfer + function evaluated at a point in the complex plane. See + :meth:`~control.StateSpace.__call__` for a more detailed description. + StateSpace instances have support for IPython LaTeX output, intended for pretty-printing in Jupyter notebooks. The LaTeX output can be configured using @@ -212,6 +242,7 @@ class StateSpace(LTI): `'partitioned'` or `'separate'`. If `'partitioned'`, the A, B, C, D matrices are shown as a single, partitioned matrix; if `'separate'`, the matrices are shown separately. + """ # Allow ndarray * StateSpace to give StateSpace._rmul_() priority @@ -296,7 +327,8 @@ def __init__(self, *args, **kwargs): elif len(args) == 5: dt = args[4] if 'dt' in kwargs: - warn('received multiple dt arguments, using positional arg dt=%s'%dt) + warn("received multiple dt arguments, " + "using positional arg dt = %s" % dt) elif len(args) == 1: try: dt = args[0].dt @@ -331,6 +363,48 @@ def __init__(self, *args, **kwargs): if remove_useless_states: self._remove_useless_states() + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system inputs. + #: + #: :meta hide-value: + ninputs = 0 + + #: Number of system outputs. + #: + #: :meta hide-value: + noutputs = 0 + + #: Number of system states. + #: + #: :meta hide-value: + nstates = 0 + + #: Dynamics matrix. + #: + #: :meta hide-value: + A = [] + + #: Input matrix. + #: + #: :meta hide-value: + B = [] + + #: Output matrix. + #: + #: :meta hide-value: + C = [] + + #: Direct term. + #: + #: :meta hide-value: + D = [] + # # Getter and setter functions for legacy state attributes # @@ -339,20 +413,25 @@ def __init__(self, *args, **kwargs): # future warning, so that users will see it. # - @property - def states(self): + def _get_states(self): warn("The StateSpace `states` attribute will be deprecated in a " "future release. Use `nstates` instead.", DeprecationWarning, stacklevel=2) return self.nstates - @states.setter - def states(self, value): + def _set_states(self, value): warn("The StateSpace `states` attribute will be deprecated in a " "future release. Use `nstates` instead.", DeprecationWarning, stacklevel=2) self.nstates = value + #: Deprecated attribute; use :attr:`nstates` instead. + #: + #: The ``state`` attribute was used to store the number of states for : a + #: state space system. It is no longer used. If you need to access the + #: number of states, use :attr:`nstates`. + states = property(_get_states, _set_states) + def _remove_useless_states(self): """Check for states that don't do anything, and remove them. @@ -626,8 +705,10 @@ def __mul__(self, other): # Check to make sure the dimensions are OK if self.ninputs != other.noutputs: - raise ValueError("C = A * B: A has %i column(s) (input(s)), \ - but B has %i row(s)\n(output(s))." % (self.ninputs, other.noutputs)) + raise ValueError( + "C = A * B: A has %i column(s) (input(s)), " + "but B has %i row(s)\n(output(s))." % + (self.ninputs, other.noutputs)) dt = common_timebase(self.dt, other.dt) # Concatenate the various arrays @@ -821,10 +902,10 @@ def horner(self, x, warn_infinite=True): out = empty((self.noutputs, self.ninputs, len(x_arr)), dtype=complex) - #TODO: can this be vectorized? + # TODO: can this be vectorized? for idx, x_idx in enumerate(x_arr): try: - out[:,:,idx] = np.dot( + out[:, :, idx] = np.dot( self.C, solve(x_idx * eye(self.nstates) - self.A, self.B)) \ + self.D @@ -837,9 +918,9 @@ def horner(self, x, warn_infinite=True): # Evaluating at a pole. Return value depends if there # is a zero at the same point or not. if x_idx in self.zero(): - out[:,:,idx] = complex(np.nan, np.nan) + out[:, :, idx] = complex(np.nan, np.nan) else: - out[:,:,idx] = complex(np.inf, np.nan) + out[:, :, idx] = complex(np.inf, np.nan) return out @@ -914,7 +995,7 @@ def feedback(self, other=1, sign=-1): other = _convert_to_statespace(other) # Check to make sure the dimensions are OK - if (self.ninputs != other.noutputs) or (self.noutputs != other.ninputs): + if self.ninputs != other.noutputs or self.noutputs != other.ninputs: raise ValueError("State space systems don't have compatible " "inputs/outputs for feedback.") dt = common_timebase(self.dt, other.dt) @@ -1288,17 +1369,17 @@ def dynamics(self, t, x, u=None): ------- dx/dt or x[t+dt] : ndarray """ - x = np.reshape(x, (-1, 1)) # force to a column in case matrix + x = np.reshape(x, (-1, 1)) # force to a column in case matrix if np.size(x) != self.nstates: raise ValueError("len(x) must be equal to number of states") if u is None: - return self.A.dot(x).reshape((-1,)) # return as row vector - else: # received t, x, and u, ignore t - u = np.reshape(u, (-1, 1)) # force to a column in case matrix + return self.A.dot(x).reshape((-1,)) # return as row vector + else: # received t, x, and u, ignore t + u = np.reshape(u, (-1, 1)) # force to column in case matrix if np.size(u) != self.ninputs: raise ValueError("len(u) must be equal to number of inputs") return self.A.dot(x).reshape((-1,)) \ - + self.B.dot(u).reshape((-1,)) # return as row vector + + self.B.dot(u).reshape((-1,)) # return as row vector def output(self, t, x, u=None): """Compute the output of the system @@ -1312,8 +1393,8 @@ def output(self, t, x, u=None): The first argument `t` is ignored because :class:`StateSpace` systems are time-invariant. It is included so that the dynamics can be passed - to most numerical integrators, such as scipy's `integrate.solve_ivp` and - for consistency with :class:`IOSystem` systems. + to most numerical integrators, such as scipy's `integrate.solve_ivp` + and for consistency with :class:`IOSystem` systems. The inputs `x` and `u` must be of the correct length for the system. @@ -1330,18 +1411,18 @@ def output(self, t, x, u=None): ------- y : ndarray """ - x = np.reshape(x, (-1, 1)) # force to a column in case matrix + x = np.reshape(x, (-1, 1)) # force to a column in case matrix if np.size(x) != self.nstates: raise ValueError("len(x) must be equal to number of states") if u is None: - return self.C.dot(x).reshape((-1,)) # return as row vector - else: # received t, x, and u, ignore t - u = np.reshape(u, (-1, 1)) # force to a column in case matrix + return self.C.dot(x).reshape((-1,)) # return as row vector + else: # received t, x, and u, ignore t + u = np.reshape(u, (-1, 1)) # force to a column in case matrix if np.size(u) != self.ninputs: raise ValueError("len(u) must be equal to number of inputs") return self.C.dot(x).reshape((-1,)) \ - + self.D.dot(u).reshape((-1,)) # return as row vector + + self.D.dot(u).reshape((-1,)) # return as row vector def _isstatic(self): """True if and only if the system has no dynamics, that is, @@ -1349,7 +1430,6 @@ def _isstatic(self): return not np.any(self.A) and not np.any(self.B) - # TODO: add discrete time check def _convert_to_statespace(sys, **kw): """Convert a system to state space form (if needed). @@ -1446,7 +1526,7 @@ def _convert_to_statespace(sys, **kw): try: D = _ssmatrix(sys) return StateSpace([], [], [], D) - except: + except Exception: raise TypeError("Can't convert given type to StateSpace system.") @@ -1679,6 +1759,7 @@ def _mimo2simo(sys, input, warn_conversion=False): return sys + def ss(*args, **kwargs): """ss(A, B, C, D[, dt]) @@ -1767,7 +1848,8 @@ def ss(*args, **kwargs): raise TypeError("ss(sys): sys must be a StateSpace or " "TransferFunction object. It is %s." % type(sys)) else: - raise ValueError("Needs 1, 4, or 5 arguments; received %i." % len(args)) + raise ValueError( + "Needs 1, 4, or 5 arguments; received %i." % len(args)) def tf2ss(*args): diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index c1c4d8006..8acd83632 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -963,7 +963,7 @@ def test_sys_naming_convention(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.InputOutputSystem.idCounter = 0 + ct.InputOutputSystem._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) assert sys.name == "sys[0]" @@ -1027,7 +1027,7 @@ def test_signals_naming_convention_0_8_4(self, tsys): ct.config.use_legacy_defaults('0.8.4') # changed delims in 0.9.0 ct.config.use_numpy_matrix(False) # np.matrix deprecated - ct.InputOutputSystem.idCounter = 0 + ct.InputOutputSystem._idCounter = 0 sys = ct.LinearIOSystem(tsys.mimo_linsys1) for statename in ["x[0]", "x[1]"]: assert statename in sys.state_index diff --git a/control/xferfcn.py b/control/xferfcn.py index 99603b253..cb3bb4d41 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -72,21 +72,49 @@ # Define module default parameter values _xferfcn_defaults = {} + class TransferFunction(LTI): """TransferFunction(num, den[, dt]) - A class for representing transfer functions + A class for representing transfer functions. The TransferFunction class is used to represent systems in transfer function form. - The main data members are 'num' and 'den', which are 2-D lists of arrays - containing MIMO numerator and denominator coefficients. For example, + Parameters + ---------- + 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 + Polynomial coefficients of the denominator + dt : None, True or float, optional + System timebase. 0 (default) indicates continuous + time, True indicates discrete time with unspecified sampling + time, positive number is discrete time with specified + sampling time, None indicates unspecified timebase (either + continuous or discrete time). + + Attributes + ---------- + ninputs, noutputs, nstates : int + Number of input, output and state variables. + num, den : 2D list of array + Polynomial coefficients of the numerator and denominator. + dt : None, True or float + System timebase. 0 (default) indicates continuous time, True indicates + discrete time with unspecified sampling time, positive number is + discrete time with specified sampling time, None indicates unspecified + timebase (either continuous or discrete time). + + Notes + ----- + The attribues 'num' and 'den' are 2-D lists of arrays containing MIMO + numerator and denominator coefficients. For example, >>> num[2][5] = numpy.array([1., 4., 8.]) - means that the numerator of the transfer function from the 6th input to the - 3rd output is set to s^2 + 4s + 8. + means that the numerator of the transfer function from the 6th input to + the 3rd output is set to s^2 + 4s + 8. A discrete time transfer function is created by specifying a nonzero 'timebase' dt when the system is constructed: @@ -105,13 +133,18 @@ class TransferFunction(LTI): The default value of dt can be changed by changing the value of ``control.config.defaults['control.default_dt']``. + A transfer function is callable and returns the value of the transfer + function evaluated at a point in the complex plane. See + :meth:`~control.TransferFunction.__call__` for a more detailed description. + The TransferFunction class defines two constants ``s`` and ``z`` that represent the differentiation and delay operators in continuous and discrete time. These can be used to create variables that allow algebraic creation of transfer functions. For example, >>> s = TransferFunction.s - >>> G = (s + 1)/(s**2 + 2*s + 1) + >>> G = (s + 1)/(s**2 + 2*s + 1) + """ # Give TransferFunction._rmul_() priority for ndarray * TransferFunction @@ -234,6 +267,45 @@ def __init__(self, *args, **kwargs): dt = config.defaults['control.default_dt'] self.dt = dt + # + # Class attributes + # + # These attributes are defined as class attributes so that they are + # documented properly. They are "overwritten" in __init__. + # + + #: Number of system inputs. + #: + #: :meta hide-value: + ninputs = 1 + + #: Number of system outputs. + #: + #: :meta hide-value: + noutputs = 1 + + #: Transfer function numerator polynomial (array) + #: + #: The numerator of the transfer function is stored as an 2D list of + #: arrays containing MIMO numerator coefficients, indexed by outputs and + #: inputs. For example, ``num[2][5]`` is the array of coefficients for + #: the numerator of the transfer function from the sixth input to the + #: third output. + #: + #: :meta hide-value: + num = [[0]] + + #: Transfer function denominator polynomial (array) + #: + #: The numerator of the transfer function is store as an 2D list of + #: arrays containing MIMO numerator coefficients, indexed by outputs and + #: inputs. For example, ``den[2][5]`` is the array of coefficients for + #: the denominator of the transfer function from the sixth input to the + #: third output. + #: + #: :meta hide-value: + den = [[0]] + def __call__(self, x, squeeze=None, warn_infinite=True): """Evaluate system's transfer function at complex frequencies. @@ -390,11 +462,13 @@ def __repr__(self): if self.issiso(): return "TransferFunction({num}, {den}{dt})".format( num=self.num[0][0].__repr__(), den=self.den[0][0].__repr__(), - dt=(isdtime(self, strict=True) and ', {}'.format(self.dt)) or '') + dt=', {}'.format(self.dt) if isdtime(self, strict=True) + else '') else: return "TransferFunction({num}, {den}{dt})".format( num=self.num.__repr__(), den=self.den.__repr__(), - dt=(isdtime(self, strict=True) and ', {}'.format(self.dt)) or '') + dt=', {}'.format(self.dt) if isdtime(self, strict=True) + else '') def _repr_latex_(self, var=None): """LaTeX representation of transfer function, for Jupyter notebook""" @@ -1047,7 +1121,7 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): if method == "matched": return _c2d_matched(self, Ts) sys = (self.num[0][0], self.den[0][0]) - if (method=='bilinear' or (method=='gbt' and alpha==0.5)) and \ + if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \ prewarp_frequency is not None: Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency else: @@ -1084,15 +1158,49 @@ def dcgain(self, warn_infinite=False): return self._dcgain(warn_infinite) def _isstatic(self): - """returns True if and only if all of the numerator and denominator - polynomials of the (possibly MIMO) transfer function are zeroth order, - that is, if the system has no dynamics. """ - for list_of_polys in self.num, self.den: - for row in list_of_polys: - for poly in row: - if len(poly) > 1: - return False - return True + """returns True if and only if all of the numerator and denominator + polynomials of the (possibly MIMO) transfer function are zeroth order, + that is, if the system has no dynamics. """ + for list_of_polys in self.num, self.den: + for row in list_of_polys: + for poly in row: + if len(poly) > 1: + return False + return True + + # Attributes for differentiation and delay + # + # These attributes are created here with sphinx docstrings so that the + # autodoc generated documentation has a description. The actual values of + # the class attributes are set at the bottom of the file to avoid problems + # with recursive calls. + + #: Differentation operator (continuous time) + #: + #: The ``s`` constant can be used to create continuous time transfer + #: functions using algebraic expressions. + #: + #: Example + #: ------- + #: >>> s = TransferFunction.s + #: >>> G = (s + 1)/(s**2 + 2*s + 1) + #: + #: :meta hide-value: + s = None + + #: Delay operator (discrete time) + #: + #: The ``z`` constant can be used to create discrete time transfer + #: functions using algebraic expressions. + #: + #: Example + #: ------- + #: >>> z = TransferFunction.z + #: >>> G = 2 * z / (4 * z**3 + 3*z - 1) + #: + #: :meta hide-value: + z = None + # c2d function contributed by Benjamin White, Oct 2012 def _c2d_matched(sysC, Ts): @@ -1297,7 +1405,7 @@ def _convert_to_transfer_function(sys, **kw): num = [[[D[i, j]] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] return TransferFunction(num, den) - except: + except Exception: raise TypeError("Can't convert given type to TransferFunction system.") @@ -1563,6 +1671,7 @@ def _clean_part(data): return data + # Define constants to represent differentiation, unit delay TransferFunction.s = TransferFunction([1, 0], [1], 0) TransferFunction.z = TransferFunction([1, 0], [1], True) diff --git a/doc/_templates/custom-class-template.rst b/doc/_templates/custom-class-template.rst new file mode 100644 index 000000000..53a76e905 --- /dev/null +++ b/doc/_templates/custom-class-template.rst @@ -0,0 +1,23 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + :inherited-members: + :special-members: + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + :nosignatures: + {% for item in methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/doc/classes.rst b/doc/classes.rst index fdf39a457..b80b7dd54 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -12,11 +12,11 @@ these directly. .. autosummary:: :toctree: generated/ + :template: custom-class-template.rst TransferFunction StateSpace FrequencyResponseData - InputOutputSystem Input/output system subclasses ============================== @@ -24,8 +24,10 @@ Input/output systems are accessed primarily via a set of subclasses that allow for linear, nonlinear, and interconnected elements: .. autosummary:: - :toctree: generated/ + :template: custom-class-template.rst + :nosignatures: + InputOutputSystem InterconnectedSystem LinearICSystem LinearIOSystem @@ -34,10 +36,14 @@ that allow for linear, nonlinear, and interconnected elements: Additional classes ================== .. autosummary:: + :template: custom-class-template.rst + :nosignatures: + DescribingFunctionNonlinearity flatsys.BasisFamily flatsys.FlatSystem flatsys.LinearFlatSystem flatsys.PolyFamily flatsys.SystemTrajectory optimal.OptimalControlProblem + optimal.OptimalControlResult diff --git a/doc/conf.py b/doc/conf.py index ebff50858..19c2970e1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,7 +48,7 @@ # If your documentation needs a minimal Sphinx version, state it here. # -# needs_sphinx = '1.0' +needs_sphinx = '3.1' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -64,8 +64,11 @@ # list of autodoc directive flags that should be automatically applied # to all autodoc directives. -autodoc_default_options = {'members': True, - 'inherited-members': True} +autodoc_default_options = { + 'members': True, + 'inherited-members': True, + 'exclude-members': '__init__, __weakref__, __repr__, __str__' +} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/control.rst b/doc/control.rst index e8a29deb9..a3e28881b 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -6,8 +6,9 @@ Function reference .. Include header information from the main control module .. automodule:: control - :no-members: - :no-inherited-members: + :no-members: + :no-inherited-members: + :no-special-members: System creation =============== diff --git a/doc/conventions.rst b/doc/conventions.rst index adcdbe96f..63f3fac2c 100644 --- a/doc/conventions.rst +++ b/doc/conventions.rst @@ -168,7 +168,7 @@ As all simulation functions return *arrays*, plotting is convenient:: The output of a MIMO system can be plotted like this:: - t, y, x = forced_response(sys, u, t) + t, y = forced_response(sys, u, t) plot(t, y[0], label='y_0') plot(t, y[1], label='y_1') diff --git a/doc/descfcn.rst b/doc/descfcn.rst index 05f6bd94a..cc3b8668d 100644 --- a/doc/descfcn.rst +++ b/doc/descfcn.rst @@ -79,6 +79,7 @@ Module classes and functions ============================ .. autosummary:: :toctree: generated/ + :template: custom-class-template.rst ~control.DescribingFunctionNonlinearity ~control.friction_backlash_nonlinearity diff --git a/doc/flatsys.rst b/doc/flatsys.rst index b6d2fe962..7599dd2af 100644 --- a/doc/flatsys.rst +++ b/doc/flatsys.rst @@ -7,6 +7,7 @@ Differentially flat systems .. automodule:: control.flatsys :no-members: :no-inherited-members: + :no-special-members: Overview of differential flatness ================================= @@ -255,21 +256,18 @@ the endpoints. Module classes and functions ============================ -Flat systems classes --------------------- .. autosummary:: :toctree: generated/ + :template: custom-class-template.rst - BasisFamily - BezierFamily - FlatSystem - LinearFlatSystem - PolyFamily - SystemTrajectory + ~control.flatsys.BasisFamily + ~control.flatsys.BezierFamily + ~control.flatsys.FlatSystem + ~control.flatsys.LinearFlatSystem + ~control.flatsys.PolyFamily + ~control.flatsys.SystemTrajectory -Flat systems functions ----------------------- .. autosummary:: :toctree: generated/ - point_to_point + ~control.flatsys.point_to_point diff --git a/doc/iosys.rst b/doc/iosys.rst index 1b160bad1..41e37cfec 100644 --- a/doc/iosys.rst +++ b/doc/iosys.rst @@ -263,9 +263,9 @@ unconnected (so be careful!). Module classes and functions ============================ -Input/output system classes ---------------------------- .. autosummary:: + :toctree: generated/ + :template: custom-class-template.rst ~control.InputOutputSystem ~control.InterconnectedSystem @@ -273,9 +273,8 @@ Input/output system classes ~control.LinearIOSystem ~control.NonlinearIOSystem -Input/output system functions ------------------------------ .. autosummary:: + :toctree: generated/ ~control.find_eqpt ~control.linearize diff --git a/doc/matlab.rst b/doc/matlab.rst index ae5688dde..c14a67e1f 100644 --- a/doc/matlab.rst +++ b/doc/matlab.rst @@ -7,6 +7,7 @@ .. automodule:: control.matlab :no-members: :no-inherited-members: + :no-special-members: Creating linear models ====================== diff --git a/doc/optimal.rst b/doc/optimal.rst index 9538c28c2..e173e430b 100644 --- a/doc/optimal.rst +++ b/doc/optimal.rst @@ -7,6 +7,7 @@ Optimal control .. automodule:: control.optimal :no-members: :no-inherited-members: + :no-special-members: Problem setup ============= @@ -276,8 +277,14 @@ Module classes and functions ============================ .. autosummary:: :toctree: generated/ + :template: custom-class-template.rst ~control.optimal.OptimalControlProblem + ~control.optimal.OptimalControlResult + +.. autosummary:: + :toctree: generated/ + ~control.optimal.solve_ocp ~control.optimal.create_mpc_iosystem ~control.optimal.input_poly_constraint