diff --git a/control/descfcn.py b/control/descfcn.py index 6c8cc9241..bfe2d1a7e 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -152,7 +152,7 @@ def describing_function( # # The describing function of a nonlinear function F() can be computed by # evaluating the nonlinearity over a sinusoid. The Fourier series for a - # static nonlinear function evaluated on a sinusoid can be written as + # nonlinear function evaluated on a sinusoid can be written as # # F(A\sin\omega t) = \sum_{k=1}^\infty M_k(A) \sin(k\omega t + \phi_k(A)) # @@ -226,10 +226,10 @@ class DescribingFunctionResponse: """Results of describing function analysis. Describing functions allow analysis of a linear I/O systems with a - static nonlinear feedback function. The DescribingFunctionResponse - class is used by the `describing_function_response` - function to return the results of a describing function analysis. The - response object can be used to obtain information about the describing + nonlinear feedback function. The DescribingFunctionResponse class + is used by the `describing_function_response` function to return + the results of a describing function analysis. The response + object can be used to obtain information about the describing function analysis or generate a Nyquist plot showing the frequency response of the linear systems and the describing function for the nonlinear element. @@ -283,16 +283,16 @@ def describing_function_response( """Compute the describing function response of a system. This function uses describing function analysis to analyze a closed - loop system consisting of a linear system with a static nonlinear - function in the feedback path. + loop system consisting of a linear system with a nonlinear function in + the feedback path. Parameters ---------- H : LTI system Linear time-invariant (LTI) system (state space, transfer function, or FRD). - F : static nonlinear function - A static nonlinearity, either a scalar function or a single-input, + F : nonlinear function + Feedback nonlinearity, either a scalar function or a single-input, single-output, static input/output system. A : list List of amplitudes to be used for the describing function plot. @@ -405,8 +405,7 @@ def describing_function_plot( Nyquist plot with describing function for a nonlinear system. This function generates a Nyquist plot for a closed loop system - consisting of a linear system with a static nonlinear function in the - feedback path. + consisting of a linear system with a nonlinearity in the feedback path. The function may be called in one of two forms: @@ -426,9 +425,9 @@ def describing_function_plot( H : LTI system Linear time-invariant (LTI) system (state space, transfer function, or FRD). - F : static nonlinear function - A static nonlinearity, either a scalar function or a single-input, - single-output, static input/output system. + F : nonlinear function + Nonlinearity in the feedback path, either a scalar function or a + single-input, single-output, static input/output system. A : list List of amplitudes to be used for the describing function plot. omega : list, optional diff --git a/control/iosys.py b/control/iosys.py index 40367595d..110552138 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -757,10 +757,6 @@ def issiso(self): """Check to see if a system is single input, single output.""" return self.ninputs == 1 and self.noutputs == 1 - def _isstatic(self): - """Check to see if a system is a static system (no states)""" - return self.nstates == 0 - # Test to see if a system is SISO def issiso(sys, strict=False): diff --git a/control/nlsys.py b/control/nlsys.py index 7073cae30..32524a9cc 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -181,7 +181,7 @@ def __call__(sys, u, params=None, squeeze=None): """ # Make sure the call makes sense - if not sys._isstatic(): + if sys.nstates != 0: raise TypeError( "function evaluation is only supported for static " "input/output systems") @@ -199,7 +199,7 @@ def __call__(sys, u, params=None, squeeze=None): def __mul__(self, other): """Multiply two input/output systems (series interconnection)""" # Convert 'other' to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented @@ -231,7 +231,7 @@ def __mul__(self, other): def __rmul__(self, other): """Pre-multiply an input/output systems by a scalar/matrix""" # Convert other to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented @@ -263,7 +263,7 @@ def __rmul__(self, other): def __add__(self, other): """Add two input/output systems (parallel interconnection)""" # Convert other to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented @@ -284,7 +284,7 @@ def __add__(self, other): def __radd__(self, other): """Parallel addition of input/output system to a compatible object.""" # Convert other to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented @@ -305,7 +305,7 @@ def __radd__(self, other): def __sub__(self, other): """Subtract two input/output systems (parallel interconnection)""" # Convert other to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented @@ -329,7 +329,7 @@ def __sub__(self, other): def __rsub__(self, other): """Parallel subtraction of I/O system to a compatible object.""" # Convert other to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) if not isinstance(other, InputOutputSystem): return NotImplemented return other - self @@ -355,6 +355,10 @@ def __truediv__(self, other): else: return NotImplemented + # Determine if a system is static (memoryless) + def _isstatic(self): + return self.nstates == 0 + def _update_params(self, params): # Update the current parameter values self._current_params = self.params.copy() @@ -484,7 +488,7 @@ def feedback(self, other=1, sign=-1, params=None): """ # Convert sys2 to an I/O system if needed - other = _convert_static_iosystem(other) + other = _convert_to_iosystem(other) # Make sure systems can be interconnected if self.noutputs != other.ninputs or other.noutputs != self.ninputs: @@ -932,6 +936,7 @@ def _out(self, t, x, u): # Make the full set of subsystem outputs to system output return self.output_map @ ylist + # Find steady state (static) inputs and outputs def _compute_static_io(self, t, x, u): # Figure out the total number of inputs and outputs (ninputs, noutputs) = self.connect_map.shape @@ -1711,8 +1716,8 @@ def ufun(t): dt = (t - T[idx-1]) / (T[idx] - T[idx-1]) return U[..., idx-1] * (1. - dt) + U[..., idx] * dt - # Check to make sure this is not a static function - if nstates == 0: # No states => map input to output + # Check to make sure see if this is a static function + if sys.nstates == 0: # Make sure the user gave a time vector for evaluation (or 'T') if t_eval is None: # User overrode t_eval with None, but didn't give us the times... @@ -2924,8 +2929,8 @@ def _process_vector_argument(arg, name, size): return val, nelem -# Utility function to create an I/O system from a static gain -def _convert_static_iosystem(sys): +# Utility function to create an I/O system (from number or array) +def _convert_to_iosystem(sys): # If we were given an I/O system, do nothing if isinstance(sys, InputOutputSystem): return sys diff --git a/control/statesp.py b/control/statesp.py index 93bee45da..2eba44df3 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -229,6 +229,9 @@ def __init__(self, *args, **kwargs): self.C = C self.D = D + # Determine if the system is static (memoryless) + static = (A.size == 0) + # # Process keyword arguments # @@ -242,7 +245,7 @@ def __init__(self, *args, **kwargs): {'inputs': B.shape[1], 'outputs': C.shape[0], 'states': A.shape[0]} name, inputs, outputs, states, dt = _process_iosys_keywords( - kwargs, defaults, static=(A.size == 0)) + kwargs, defaults, static=static) # Create updfcn and outfcn updfcn = lambda t, x, u, params: \ @@ -257,7 +260,7 @@ def __init__(self, *args, **kwargs): states=states, dt=dt, **kwargs) # Reset shapes if the system is static - if self._isstatic(): + if static: A.shape = (0, 0) B.shape = (0, self.ninputs) C.shape = (self.noutputs, 0) diff --git a/control/xferfcn.py b/control/xferfcn.py index ca8e5d114..16d7c5054 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -219,15 +219,22 @@ def __init__(self, *args, **kwargs): raise ValueError("display_format must be 'poly' or 'zpk'," " got '%s'" % self.display_format) - # Determine if the transfer function is static (needed for dt) + # + # Determine if the transfer function is static (memoryless) + # + # True if and only if all of the numerator and denominator + # polynomials of the (MIMO) transfer function are zeroth order. + # static = True for arr in [num, den]: + # Iterate using refs_OK since num and den are ndarrays of ndarrays for poly_ in np.nditer(arr, flags=['refs_ok']): if poly_.item().size > 1: static = False break if not static: break + self._static = static # retain for later usage defaults = args[0] if len(args) == 1 else \ {'inputs': num.shape[1], 'outputs': num.shape[0]} @@ -1283,16 +1290,9 @@ def dcgain(self, warn_infinite=False): """ return self._dcgain(warn_infinite) + # Determine if a system is static (memoryless) 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 + return self._static # Check done at initialization # Attributes for differentiation and delay # diff --git a/doc/descfcn.rst b/doc/descfcn.rst index 55d218f85..edff8603b 100644 --- a/doc/descfcn.rst +++ b/doc/descfcn.rst @@ -6,14 +6,21 @@ Describing Functions ==================== For nonlinear systems consisting of a feedback connection between a -linear system and a static nonlinearity, it is possible to obtain a +linear system and a nonlinearity, it is possible to obtain a generalization of Nyquist's stability criterion based on the idea of describing functions. The basic concept involves approximating the -response of a static nonlinearity to an input :math:`u = A e^{j \omega -t}` as an output :math:`y = N(A) (A e^{j \omega t})`, where :math:`N(A) -\in \mathbb{C}` represents the (amplitude-dependent) gain and phase +response of a nonlinearity to an input :math:`u = A e^{j \omega t}` as +an output :math:`y = N(A) (A e^{j \omega t})`, where :math:`N(A) \in +\mathbb{C}` represents the (amplitude-dependent) gain and phase associated with the nonlinearity. +In the most common case, the nonlinearity will be a static, +time-invariant nonlinear function :math:`y = h(u)`. However, +describing functions can be defined for nonlinear input/output systems +that have some internal memory, such as hysteresis or backlash. For +simplicity, we take the nonlinearity to be static (memoryless) in the +description below, unless otherwise specified. + Stability analysis of a linear system :math:`H(s)` with a feedback nonlinearity :math:`F(x)` is done by looking for amplitudes :math:`A` and frequencies :math:`\omega` such that diff --git a/doc/nlsys.rst b/doc/nlsys.rst index f063cd13d..31c2656e4 100644 --- a/doc/nlsys.rst +++ b/doc/nlsys.rst @@ -23,6 +23,11 @@ Discrete time systems are also supported and have dynamics of the form x[t+1] &= f(t, x[t], u[t], \theta), \\ y[t] &= h(t, x[t], u[t], \theta). +A nonlinear input/output model is said to be "static" if the output +:math:`y(t)` at any given time :math:`t` depends only on the input +:math:`u(t)` at that same time :math:`t` and not on past or future +values of :math:`u`. + .. _sec-nonlinear-models: @@ -47,7 +52,9 @@ dynamics of the system can be in continuous or discrete time (use the The output function `outfcn` is used to specify the outputs of the system and has the same calling signature as `updfcn`. If it is not specified, then the output of the system is set equal to the system -state. Otherwise, it should return an array of shape (p,). +state. Otherwise, it should return an array of shape (p,). If a +input/output system is static, the state `x` should still be passed to +the output function, but the state is ignored. Note that the number of states, inputs, and outputs should generally be explicitly specified, although some operations can infer the