33# RMM, 23 Jan 2021
44#
55# This module adds functions for carrying out analysis of systems with
6- # static nonlinear feedback functions using describing functions.
6+ # memoryless nonlinear feedback functions using describing functions.
77#
88
99"""The :mod:~control.descfcn` module contains function for performing
10- closed loop analysis of systems with static nonlinearities using
10+ closed loop analysis of systems with memoryless nonlinearities using
1111describing function analysis.
1212
1313"""
2626
2727# Class for nonlinearities with a built-in describing function
2828class DescribingFunctionNonlinearity ():
29- """Base class for nonlinear functions with a describing function
29+ """Base class for nonlinear systems with a describing function
3030
3131 This class is intended to be used as a base class for nonlinear functions
32- that have a analytically defined describing function (accessed via the
33- :meth:`describing_function` method). Objects using this class should also
34- implement a `call` method that evaluates the nonlinearity at a given point
35- and an `_isstatic` method that is `True` if the nonlinearity has no
36- internal state.
32+ that have an analytically defined describing function. Subclasses should
33+ override the `__call__` and `describing_function` methods and (optionally)
34+ the `_isstatic` method (should be `False` if `__call__` updates the
35+ instance state).
3736
3837 """
3938 def __init__ (self ):
40- """Initailize a describing function nonlinearity"""
39+ """Initailize a describing function nonlinearity (optional) """
4140 pass
4241
4342 def __call__ (self , A ):
43+ """Evaluate the nonlinearity at a (scalar) input value"""
4444 raise NotImplementedError (
4545 "__call__() not implemented for this function (internal error)" )
4646
@@ -56,9 +56,15 @@ def describing_function(self, A):
5656 "describing function not implemented for this function" )
5757
5858 def _isstatic (self ):
59- """Return True if the function has not internal state"""
60- raise NotImplementedError (
61- "_isstatic() not implemented for this function (internal error)" )
59+ """Return True if the function has no internal state (memoryless)
60+
61+ This internal function is used to optimize numerical computation of
62+ the describing function. It can be set to `True` if the instance
63+ maintains no internal memory of the instance state. Assumed False by
64+ default.
65+
66+ """
67+ return False
6268
6369 # Utility function used to compute common describing functions
6470 def _f (self , x ):
@@ -70,10 +76,10 @@ def describing_function(
7076 F , A , num_points = 100 , zero_check = True , try_method = True ):
7177 """Numerical compute the describing function of a nonlinear function
7278
73- The describing function of a static nonlinear function is given by
74- magnitude and phase of the first harmonic of the function when evaluated
75- along a sinusoidal input :math:`a \\ sin \\ omega t`. This function returns
76- the magnitude and phase of the describing function at amplitude :math:`A`.
79+ The describing function of a nonlinearity is given by magnitude and phase
80+ of the first harmonic of the function when evaluated along a sinusoidal
81+ input :math:`A \\ sin \\ omega t`. This function returns the magnitude and
82+ phase of the describing function at amplitude :math:`A`.
7783
7884 Parameters
7985 ----------
@@ -119,11 +125,15 @@ def describing_function(
119125 """
120126 # If there is an analytical solution, trying using that first
121127 if try_method and hasattr (F , 'describing_function' ):
122- # Go through all of the amplitudes we were given
123- df = []
124- for a in np .atleast_1d (A ):
125- df .append (F .describing_function (a ))
126- return np .array (df ).reshape (np .shape (A ))
128+ try :
129+ # Go through all of the amplitudes we were given
130+ df = []
131+ for a in np .atleast_1d (A ):
132+ df .append (F .describing_function (a ))
133+ return np .array (df ).reshape (np .shape (A ))
134+ except NotImplementedError :
135+ # Drop through and do the numerical computation
136+ pass
127137
128138 #
129139 # The describing function of a nonlinear function F() can be computed by
@@ -136,24 +146,24 @@ def describing_function(
136146 #
137147 # N(A) = M_1(A) e^{j \phi_1(A)} / A
138148 #
139- # To compute this, we compute F(\ theta) for \theta between 0 and 2 \pi,
140- # use the identities
149+ # To compute this, we compute F(A \sin\ theta) for \theta between 0 and 2
150+ # \pi, use the identities
141151 #
142152 # \sin(\theta + \phi) = \sin\theta \cos\phi + \cos\theta \sin\phi
143153 # \int_0^{2\pi} \sin^2 \theta d\theta = \pi
144154 # \int_0^{2\pi} \cos^2 \theta d\theta = \pi
145155 #
146156 # and then integrate the product against \sin\theta and \cos\theta to obtain
147157 #
148- # \int_0^{2\pi} F(a \sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi
149- # \int_0^{2\pi} F(a \sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi
158+ # \int_0^{2\pi} F(A \sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi
159+ # \int_0^{2\pi} F(A \sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi
150160 #
151161 # From these we can compute M1 and \phi.
152162 #
153163
154- # Evaluate over a full range of angles
155- theta = np .linspace (0 , 2 * np . pi , num_points )
156- dtheta = theta [ 1 ] - theta [ 0 ]
164+ # Evaluate over a full range of angles (leave off endpoint a la DFT)
165+ theta , dtheta = np .linspace (
166+ 0 , 2 * np . pi , num_points , endpoint = False , retstep = True )
157167 sin_theta = np .sin (theta )
158168 cos_theta = np .cos (theta )
159169
@@ -175,10 +185,10 @@ def describing_function(
175185 elif a < 0 :
176186 raise ValueError ("cannot evaluate describing function for A < 0" )
177187
178- # Save the scaling factor for to make the formulas simpler
188+ # Save the scaling factor to make the formulas simpler
179189 scale = dtheta / np .pi / a
180190
181- # Evaluate the function (twice) along a sinusoid (for internal state)
191+ # Evaluate the function along a sinusoid
182192 F_eval = np .array ([F (x ) for x in a * sin_theta ]).squeeze ()
183193
184194 # Compute the prjections onto sine and cosine
@@ -353,7 +363,7 @@ def __init__(self, ub=1, lb=None):
353363 self .ub = ub
354364
355365 def __call__ (self , x ):
356- return np .maximum ( self .lb , np . minimum ( x , self .ub ) )
366+ return np .clip ( x , self .lb , self .ub )
357367
358368 def _isstatic (self ):
359369 return True
@@ -453,18 +463,12 @@ def __init__(self, b):
453463 self .center = 0 # current center position
454464
455465 def __call__ (self , x ):
456- # Convert input to an array
457- x_array = np .array (x )
458-
459- y = []
460- for x in np .atleast_1d (x_array ):
461- # If we are outside the backlash, move and shift the center
462- if x - self .center > self .b / 2 :
463- self .center = x - self .b / 2
464- elif x - self .center < - self .b / 2 :
465- self .center = x + self .b / 2
466- y .append (self .center )
467- return (np .array (y ).reshape (x_array .shape ))
466+ # If we are outside the backlash, move and shift the center
467+ if x - self .center > self .b / 2 :
468+ self .center = x - self .b / 2
469+ elif x - self .center < - self .b / 2 :
470+ self .center = x + self .b / 2
471+ return self .center
468472
469473 def _isstatic (self ):
470474 return False
0 commit comments