1919from warnings import warn
2020
2121from .freqplot import nyquist_response
22+ from . import config
2223
2324__all__ = ['describing_function' , 'describing_function_plot' ,
24- 'DescribingFunctionNonlinearity' , 'friction_backlash_nonlinearity' ,
25- 'relay_hysteresis_nonlinearity' , 'saturation_nonlinearity' ]
25+ 'describing_function_response' , 'DescribingFunctionNonlinearity' ,
26+ 'friction_backlash_nonlinearity' , 'relay_hysteresis_nonlinearity' ,
27+ 'saturation_nonlinearity' ]
2628
2729# Class for nonlinearities with a built-in describing function
2830class DescribingFunctionNonlinearity ():
@@ -205,14 +207,41 @@ def describing_function(
205207 # Return the values in the same shape as they were requested
206208 return retdf
207209
210+ #
211+ # Describing function response/plot
212+ #
208213
209- def describing_function_plot (
210- H , F , A , omega = None , refine = True , label = "%5.2g @ %-5.2g" ,
211- warn = None , ** kwargs ):
212- """Plot a Nyquist plot with a describing function for a nonlinear system.
214+ # Simple class to store the describing function response
215+ class DescribingFunctionResponse :
216+ def __init__ (self , response , N_vals , positions , intersections ):
217+ self .response = response
218+ self .N_vals = N_vals
219+ self .positions = positions
220+ self .intersections = intersections
221+
222+ def plot (self , ** kwargs ):
223+ return describing_function_plot (self , ** kwargs )
224+
225+ # Implement iter, getitem, len to allow recovering the intersections
226+ def __iter__ (self ):
227+ return iter (self .intersections )
228+
229+ def __getitem__ (self , index ):
230+ return list (self .__iter__ ())[index ]
231+
232+ def __len__ (self ):
233+ return len (self .intersections )
213234
214- This function generates a Nyquist plot for a closed loop system consisting
215- of a linear system with a static nonlinear function in the feedback path.
235+
236+ # Compute the describing function response + intersections
237+ def describing_function_response (
238+ H , F , A , omega = None , refine = True , warn_nyquist = None ,
239+ plot = False , check_kwargs = True , ** kwargs ):
240+ """Compute the describing function response of a system.
241+
242+ This function uses describing function analysis to analyze a closed
243+ loop system consisting of a linear system with a static nonlinear
244+ function in the feedback path.
216245
217246 Parameters
218247 ----------
@@ -226,10 +255,7 @@ def describing_function_plot(
226255 List of amplitudes to be used for the describing function plot.
227256 omega : list, optional
228257 List of frequencies to be used for the linear system Nyquist curve.
229- label : str, optional
230- Formatting string used to label intersection points on the Nyquist
231- plot. Defaults to "%5.2g @ %-5.2g". Set to `None` to omit labels.
232- warn : bool, optional
258+ warn_nyquist : bool, optional
233259 Set to True to turn on warnings generated by `nyquist_plot` or False
234260 to turn off warnings. If not set (or set to None), warnings are
235261 turned off if omega is specified, otherwise they are turned on.
@@ -249,31 +275,27 @@ def describing_function_plot(
249275 >>> H_simple = ct.tf([8], [1, 2, 2, 1])
250276 >>> F_saturation = ct.saturation_nonlinearity(1)
251277 >>> amp = np.linspace(1, 4, 10)
252- >>> ct.describing_function_plot (H_simple, F_saturation, amp) # doctest: +SKIP
278+ >>> ct.describing_function_response (H_simple, F_saturation, amp) # doctest: +SKIP
253279 [(3.343844998258643, 1.4142293090899216)]
254280
255281 """
256282 # Decide whether to turn on warnings or not
257- if warn is None :
283+ if warn_nyquist is None :
258284 # Turn warnings on unless omega was specified
259- warn = omega is None
285+ warn_nyquist = omega is None
260286
261287 # Start by drawing a Nyquist curve
262288 response = nyquist_response (
263- H , omega , warn_encirclements = warn , warn_nyquist = warn ,
264- check_kwargs = False , ** kwargs )
265- response .plot (** kwargs )
289+ H , omega , warn_encirclements = warn_nyquist , warn_nyquist = warn_nyquist ,
290+ check_kwargs = check_kwargs , ** kwargs )
266291 H_omega , H_vals = response .contour .imag , H (response .contour )
267292
268293 # Compute the describing function
269294 df = describing_function (F , A )
270295 N_vals = - 1 / df
271296
272- # Now add the describing function curve to the plot
273- plt .plot (N_vals .real , N_vals .imag )
274-
275297 # Look for intersection points
276- intersections = []
298+ positions , intersections = [], []
277299 for i in range (N_vals .size - 1 ):
278300 for j in range (H_vals .size - 1 ):
279301 intersect = _find_intersection (
@@ -306,17 +328,99 @@ def _cost(x):
306328 else :
307329 a_final , omega_final = res .x [0 ], res .x [1 ]
308330
309- # Add labels to the intersection points
310- if isinstance (label , str ):
311- pos = H (1j * omega_final )
312- plt .text (pos .real , pos .imag , label % (a_final , omega_final ))
313- elif label is not None or label is not False :
314- raise ValueError ("label must be formatting string or None" )
331+ pos = H (1j * omega_final )
315332
316333 # Save the final estimate
334+ positions .append (pos )
317335 intersections .append ((a_final , omega_final ))
318336
319- return intersections
337+ return DescribingFunctionResponse (
338+ response , N_vals , positions , intersections )
339+
340+
341+ def describing_function_plot (
342+ * sysdata , label = "%5.2g @ %-5.2g" , ** kwargs ):
343+ """Plot a Nyquist plot with a describing function for a nonlinear system.
344+
345+ This function generates a Nyquist plot for a closed loop system
346+ consisting of a linear system with a static nonlinear function in the
347+ feedback path.
348+
349+ Parameters
350+ ----------
351+ H : LTI system
352+ Linear time-invariant (LTI) system (state space, transfer function, or
353+ FRD)
354+ F : static nonlinear function
355+ A static nonlinearity, either a scalar function or a single-input,
356+ single-output, static input/output system.
357+ A : list
358+ List of amplitudes to be used for the describing function plot.
359+ omega : list, optional
360+ List of frequencies to be used for the linear system Nyquist curve.
361+ refine : bool, optional
362+ If True (default), refine the location of the intersection of the
363+ Nyquist curve for the linear system and the describing function to
364+ determine the intersection point
365+ label : str, optional
366+ Formatting string used to label intersection points on the Nyquist
367+ plot. Defaults to "%5.2g @ %-5.2g". Set to `None` to omit labels.
368+
369+ Returns
370+ -------
371+ intersections : 1D array of 2-tuples or None
372+ A list of all amplitudes and frequencies in which :math:`H(j\\ omega)
373+ N(a) = -1`, where :math:`N(a)` is the describing function associated
374+ with `F`, or `None` if there are no such points. Each pair represents
375+ a potential limit cycle for the closed loop system with amplitude
376+ given by the first value of the tuple and frequency given by the
377+ second value.
378+
379+ Examples
380+ --------
381+ >>> H_simple = ct.tf([8], [1, 2, 2, 1])
382+ >>> F_saturation = ct.saturation_nonlinearity(1)
383+ >>> amp = np.linspace(1, 4, 10)
384+ >>> ct.describing_function_plot(H_simple, F_saturation, amp) # doctest: +SKIP
385+ [(3.343844998258643, 1.4142293090899216)]
386+
387+ """
388+ # Process keywords
389+ warn_nyquist = config ._process_legacy_keyword (
390+ kwargs , 'warn' , 'warn_nyquist' , kwargs .pop ('warn_nyquist' , None ))
391+
392+ if label not in (False , None ) and not isinstance (label , str ):
393+ raise ValueError ("label must be formatting string, False, or None" )
394+
395+ # Get the describing function response
396+ if len (sysdata ) == 3 :
397+ sysdata = sysdata + (None , ) # set omega to default value
398+ if len (sysdata ) == 4 :
399+ dfresp = describing_function_response (
400+ * sysdata , refine = kwargs .pop ('refine' , True ),
401+ warn_nyquist = warn_nyquist )
402+ elif len (sysdata ) == 1 :
403+ dfresp = sysdata [0 ]
404+ else :
405+ raise TypeError ("1, 3, or 4 position arguments required" )
406+
407+ # Create a list of lines for the output
408+ out = np .empty (2 , dtype = object )
409+
410+ # Plot the Nyquist response
411+ out [0 ] = dfresp .response .plot (** kwargs )[0 ]
412+
413+ # Add the describing function curve to the plot
414+ lines = plt .plot (dfresp .N_vals .real , dfresp .N_vals .imag )
415+ out [1 ] = lines
416+
417+ # Label the intersection points
418+ if label :
419+ for pos , (a , omega ) in zip (dfresp .positions , dfresp .intersections ):
420+ # Add labels to the intersection points
421+ plt .text (pos .real , pos .imag , label % (a , omega ))
422+
423+ return out
320424
321425
322426# Utility function to figure out whether two line segments intersection
0 commit comments