Skip to content

Commit e82e2af

Browse files
committed
add unit tests for exceptions/warnings + cleanup
1 parent 3b2f802 commit e82e2af

File tree

3 files changed

+82
-18
lines changed

3 files changed

+82
-18
lines changed

control/descfcn.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,15 @@ def describing_function_plot(
254254
if refine:
255255
# Refine the answer to get more accuracy
256256
def _cost(x):
257+
# If arguments are invalid, return a "large" value
258+
# Note: imposing bounds messed up the optimization (?)
259+
if x[0] < 0 or x[1] < 0:
260+
return 1
257261
return abs(1 + H(1j * x[1]) *
258262
describing_function(F, x[0]))**2
259-
res = scipy.optimize.minimize(_cost, [a_guess, omega_guess])
263+
res = scipy.optimize.minimize(
264+
_cost, [a_guess, omega_guess])
265+
# bounds=[(A[i], A[i+1]), (H_omega[j], H_omega[j+1])])
260266

261267
if not res.success:
262268
warn("not able to refine result; returning estimate")
@@ -322,6 +328,9 @@ class saturation_nonlinearity(DescribingFunctionNonlinearity):
322328
323329
"""
324330
def __init__(self, ub=1, lb=None):
331+
# Create the describing function nonlinearity object
332+
super(saturation_nonlinearity, self).__init__()
333+
325334
# Process arguments
326335
if lb == None:
327336
# Only received one argument; assume symmetric around zero
@@ -341,6 +350,10 @@ def isstatic(self):
341350
return True
342351

343352
def describing_function(self, A):
353+
# Check to make sure the amplitude is positive
354+
if A < 0:
355+
raise ValueError("cannot evaluate describing function for A < 0")
356+
344357
if self.lb <= A and A <= self.ub:
345358
return 1.
346359
else:
@@ -368,6 +381,9 @@ class relay_hysteresis_nonlinearity(DescribingFunctionNonlinearity):
368381
369382
"""
370383
def __init__(self, b, c):
384+
# Create the describing function nonlinearity object
385+
super(relay_hysteresis_nonlinearity, self).__init__()
386+
371387
# Initialize the state to bottom branch
372388
self.branch = -1 # lower branch
373389
self.b = b # relay output value
@@ -389,16 +405,16 @@ def __call__(self, x):
389405
def isstatic(self):
390406
return False
391407

392-
def describing_function(self, a):
393-
def f(x):
394-
return math.copysign(1, x) if abs(x) > 1 else \
395-
(math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi
408+
def describing_function(self, A):
409+
# Check to make sure the amplitude is positive
410+
if A < 0:
411+
raise ValueError("cannot evaluate describing function for A < 0")
396412

397-
if a < self.c:
413+
if A < self.c:
398414
return np.nan
399415

400-
df_real = 4 * self.b * math.sqrt(1 - (self.c/a)**2) / (a * math.pi)
401-
df_imag = -4 * self.b * self.c / (math.pi * a**2)
416+
df_real = 4 * self.b * math.sqrt(1 - (self.c/A)**2) / (A * math.pi)
417+
df_imag = -4 * self.b * self.c / (math.pi * A**2)
402418
return df_real + 1j * df_imag
403419

404420

@@ -421,6 +437,9 @@ class backlash_nonlinearity(DescribingFunctionNonlinearity):
421437
"""
422438

423439
def __init__(self, b):
440+
# Create the describing function nonlinearity object
441+
super(backlash_nonlinearity, self).__init__()
442+
424443
self.b = b # backlash distance
425444
self.center = 0 # current center position
426445

@@ -442,6 +461,10 @@ def isstatic(self):
442461
return False
443462

444463
def describing_function(self, A):
464+
# Check to make sure the amplitude is positive
465+
if A < 0:
466+
raise ValueError("cannot evaluate describing function for A < 0")
467+
445468
if A <= self.b/2:
446469
return 0
447470

control/iosys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ def __call__(sys, u, squeeze=None, params=None):
822822
# If we received any parameters, update them before calling _out()
823823
if params is not None:
824824
sys._update_params(params)
825-
825+
826826
# Evaluate the function on the argument
827827
out = sys._out(0, np.array((0,)), np.asarray(u))
828828
_, out = _process_time_response(sys, [], out, [], squeeze=squeeze)

control/tests/descfcn_test.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
import numpy as np
1313
import control as ct
1414
import 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
3241
def 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
7180
def 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+
113128
def 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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy