12
12
import numpy as np
13
13
import control as ct
14
14
import math
15
+ from control .descfcn import saturation_nonlinearity , backlash_nonlinearity , \
16
+ relay_hysteresis_nonlinearity
17
+
15
18
16
- class saturation ():
19
+ # Static function via a class
20
+ class saturation_class ():
17
21
# Static nonlinear saturation function
18
22
def __call__ (self , x , lb = - 1 , ub = 1 ):
19
23
return np .maximum (lb , np .minimum (x , ub ))
@@ -27,10 +31,15 @@ def describing_function(self, a):
27
31
return 2 / math .pi * (math .asin (b ) + b * math .sqrt (1 - b ** 2 ))
28
32
29
33
34
+ # Static function without a class
35
+ def saturation (x ):
36
+ return np .maximum (- 1 , np .minimum (x , 1 ))
37
+
38
+
30
39
# Static nonlinear system implementing saturation
31
40
@pytest .fixture
32
41
def satsys ():
33
- satfcn = saturation ()
42
+ satfcn = saturation_class ()
34
43
def _satfcn (t , x , u , params ):
35
44
return satfcn (u )
36
45
return ct .NonlinearIOSystem (None , outfcn = _satfcn , input = 1 , output = 1 )
@@ -65,16 +74,16 @@ def _misofcn(t, x, u, params={}):
65
74
np .testing .assert_array_equal (miso_sys ([0 , 0 ]), [0 ])
66
75
np .testing .assert_array_equal (miso_sys ([0 , 0 ]), [0 ])
67
76
np .testing .assert_array_equal (miso_sys ([0 , 0 ], squeeze = True ), [0 ])
68
-
77
+
69
78
70
79
# Test saturation describing function in multiple ways
71
80
def test_saturation_describing_function (satsys ):
72
- satfcn = saturation ()
73
-
81
+ satfcn = saturation_class ()
82
+
74
83
# Store the analytic describing function for comparison
75
84
amprange = np .linspace (0 , 10 , 100 )
76
85
df_anal = [satfcn .describing_function (a ) for a in amprange ]
77
-
86
+
78
87
# Compute describing function for a static function
79
88
df_fcn = [ct .describing_function (satfcn , a ) for a in amprange ]
80
89
np .testing .assert_almost_equal (df_fcn , df_anal , decimal = 3 )
@@ -87,8 +96,9 @@ def test_saturation_describing_function(satsys):
87
96
df_arr = ct .describing_function (satsys , amprange )
88
97
np .testing .assert_almost_equal (df_arr , df_anal , decimal = 3 )
89
98
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 )
92
102
93
103
94
104
@pytest .mark .parametrize ("fcn, amin, amax" , [
@@ -100,7 +110,7 @@ def test_describing_function(fcn, amin, amax):
100
110
# Store the analytic describing function for comparison
101
111
amprange = np .linspace (amin , amax , 100 )
102
112
df_anal = [fcn .describing_function (a ) for a in amprange ]
103
-
113
+
104
114
# Compute describing function on an array of values
105
115
df_arr = ct .describing_function (
106
116
fcn , amprange , zero_check = False , try_method = False )
@@ -110,6 +120,11 @@ def test_describing_function(fcn, amin, amax):
110
120
df_meth = ct .describing_function (fcn , amprange , zero_check = False )
111
121
np .testing .assert_almost_equal (df_meth , df_anal , decimal = 1 )
112
122
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
+
113
128
def test_describing_function_plot ():
114
129
# Simple linear system with at most 1 intersection
115
130
H_simple = ct .tf ([1 ], [1 , 2 , 2 , 1 ])
@@ -141,3 +156,29 @@ def test_describing_function_plot():
141
156
np .testing .assert_almost_equal (
142
157
- 1 / ct .describing_function (F_backlash , a ),
143
158
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