1212import numpy as np
1313import control as ct
1414import math
15+ from control .descfcn import saturation_nonlinearity , backlash_nonlinearity , \
16+ relay_hysteresis_nonlinearity
17+
1518
16- class saturation ():
19+ # Static function via a class
20+ class saturation_class ():
1721 # Static nonlinear saturation function
1822 def __call__ (self , x , lb = - 1 , ub = 1 ):
1923 return np .maximum (lb , np .minimum (x , ub ))
@@ -27,10 +31,15 @@ def describing_function(self, a):
2731 return 2 / math .pi * (math .asin (b ) + b * math .sqrt (1 - b ** 2 ))
2832
2933
34+ # Static function without a class
35+ def saturation (x ):
36+ return np .maximum (- 1 , np .minimum (x , 1 ))
37+
38+
3039# Static nonlinear system implementing saturation
3140@pytest .fixture
3241def satsys ():
33- satfcn = saturation ()
42+ satfcn = saturation_class ()
3443 def _satfcn (t , x , u , params ):
3544 return satfcn (u )
3645 return ct .NonlinearIOSystem (None , outfcn = _satfcn , input = 1 , output = 1 )
@@ -65,16 +74,16 @@ def _misofcn(t, x, u, params={}):
6574 np .testing .assert_array_equal (miso_sys ([0 , 0 ]), [0 ])
6675 np .testing .assert_array_equal (miso_sys ([0 , 0 ]), [0 ])
6776 np .testing .assert_array_equal (miso_sys ([0 , 0 ], squeeze = True ), [0 ])
68-
77+
6978
7079# Test saturation describing function in multiple ways
7180def test_saturation_describing_function (satsys ):
72- satfcn = saturation ()
73-
81+ satfcn = saturation_class ()
82+
7483 # Store the analytic describing function for comparison
7584 amprange = np .linspace (0 , 10 , 100 )
7685 df_anal = [satfcn .describing_function (a ) for a in amprange ]
77-
86+
7887 # Compute describing function for a static function
7988 df_fcn = [ct .describing_function (satfcn , a ) for a in amprange ]
8089 np .testing .assert_almost_equal (df_fcn , df_anal , decimal = 3 )
@@ -87,8 +96,9 @@ def test_saturation_describing_function(satsys):
8796 df_arr = ct .describing_function (satsys , amprange )
8897 np .testing .assert_almost_equal (df_arr , df_anal , decimal = 3 )
8998
90- from control .descfcn import saturation_nonlinearity , backlash_nonlinearity , \
91- relay_hysteresis_nonlinearity
99+ # Evaluate static function at a negative amplitude
100+ with pytest .raises (ValueError , match = "cannot evaluate" ):
101+ ct .describing_function (saturation , - 1 )
92102
93103
94104@pytest .mark .parametrize ("fcn, amin, amax" , [
@@ -100,7 +110,7 @@ def test_describing_function(fcn, amin, amax):
100110 # Store the analytic describing function for comparison
101111 amprange = np .linspace (amin , amax , 100 )
102112 df_anal = [fcn .describing_function (a ) for a in amprange ]
103-
113+
104114 # Compute describing function on an array of values
105115 df_arr = ct .describing_function (
106116 fcn , amprange , zero_check = False , try_method = False )
@@ -110,6 +120,11 @@ def test_describing_function(fcn, amin, amax):
110120 df_meth = ct .describing_function (fcn , amprange , zero_check = False )
111121 np .testing .assert_almost_equal (df_meth , df_anal , decimal = 1 )
112122
123+ # Make sure that evaluation at negative amplitude generates an exception
124+ with pytest .raises (ValueError , match = "cannot evaluate" ):
125+ ct .describing_function (fcn , - 1 )
126+
127+
113128def test_describing_function_plot ():
114129 # Simple linear system with at most 1 intersection
115130 H_simple = ct .tf ([1 ], [1 , 2 , 2 , 1 ])
@@ -141,3 +156,29 @@ def test_describing_function_plot():
141156 np .testing .assert_almost_equal (
142157 - 1 / ct .describing_function (F_backlash , a ),
143158 H_multiple (1j * w ), decimal = 5 )
159+
160+ def test_describing_function_exceptions ():
161+ # Describing function with non-zero bias
162+ with pytest .warns (UserWarning , match = "asymmetric" ):
163+ saturation = ct .descfcn .saturation_nonlinearity (lb = - 1 , ub = 2 )
164+ assert saturation (- 3 ) == - 1
165+ assert saturation (3 ) == 2
166+
167+ # Turn off the bias check
168+ bias = ct .describing_function (saturation , 0 , zero_check = False )
169+
170+ # Function should evaluate to zero at zero amplitude
171+ f = lambda x : x + 0.5
172+ with pytest .raises (ValueError , match = "must evaluate to zero" ):
173+ bias = ct .describing_function (f , 0 , zero_check = True )
174+
175+ # Evaluate at a negative amplitude
176+ with pytest .raises (ValueError , match = "cannot evaluate" ):
177+ ct .describing_function (saturation , - 1 )
178+
179+ # Describing function with bad label
180+ H_simple = ct .tf ([8 ], [1 , 2 , 2 , 1 ])
181+ F_saturation = ct .descfcn .saturation_nonlinearity (1 )
182+ amp = np .linspace (1 , 4 , 10 )
183+ with pytest .raises (ValueError , match = "formatting string" ):
184+ ct .describing_function_plot (H_simple , F_saturation , amp , label = 1 )
0 commit comments