From cda5640100e38a14c6aa842c205ddf29e8332653 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 24 Jan 2021 16:15:13 -0800 Subject: [PATCH 1/8] initial implementation of describing function computation --- control/__init__.py | 1 + control/iosys.py | 21 +++ control/nltools.py | 264 ++++++++++++++++++++++++++++++++++ control/tests/nltools_test.py | 106 ++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 control/nltools.py create mode 100644 control/tests/nltools_test.py diff --git a/control/__init__.py b/control/__init__.py index 7daa39b3e..f728e1ae3 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -55,6 +55,7 @@ from .mateqn import * from .modelsimp import * from .nichols import * +from .nltools import * from .phaseplot import * from .pzmap import * from .rlocus import * diff --git a/control/iosys.py b/control/iosys.py index 50851cbf0..61f820bea 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -450,6 +450,10 @@ def issiso(self): """Check to see if a system is single input, single output""" return self.ninputs == 1 and self.noutputs == 1 + def isstatic(self): + """Check to see if a system is a static system (no states)""" + return self.nstates == 0 + def feedback(self, other=1, sign=-1, params={}): """Feedback interconnection between two input/output systems @@ -807,6 +811,23 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, # Initialize current parameters to default parameters self._current_params = params.copy() + # Return the value of a static nonlinear system + def __call__(sys, u, squeeze=None, params=None): + # Make sure the call makes sense + if not sys.isstatic(): + raise TypeError( + "function evaluation is only supported for static " + "input/output systems") + + # If we received any parameters, update them before calling _out() + if params is not None: + sys._update_params(params) + + # Evaluate the function on the argument + out = sys._out(0, np.array((0,)), np.asarray(u)) + _, out = _process_time_response(sys, [], out, [], squeeze=squeeze) + return out + def _update_params(self, params, warning=False): # Update the current parameter values self._current_params = self.params.copy() diff --git a/control/nltools.py b/control/nltools.py new file mode 100644 index 000000000..40d64cd5f --- /dev/null +++ b/control/nltools.py @@ -0,0 +1,264 @@ +# nltools.py - nonlinear feedback analysis +# +# RMM, 23 Jan 2021 +# +# This module adds functions for carrying out analysis of systems with +# static nonlinear feedback functions using the circle criterion and +# describing functions. +# + +"""The :mod:~control.nltools` module contains function for performing closed +loop analysis of systems with static nonlinearities. It is built around the +basic structure required to apply the circle criterion and describing function +analysis. + +""" + +import math +import numpy as np +import matplotlib.pyplot as plt +from numpy import where, dstack, diff, meshgrid +from warnings import warn + +from .freqplot import nyquist_plot + +__all__ = ['describing_function', 'describing_function_plot', 'sector_bounds'] + +def sector_bounds(fcn): + raise NotImplementedError("function not currently implemented") + + +def describing_function(fcn, amp, num_points=100, zero_check=True): + """Numerical compute the describing function of a nonlinear function + + The describing function of a static nonlinear function is given by + magnitude and phase of the first harmonic of the function when evaluated + along a sinusoidal input :math:`a \\sin \\omega t`. This function returns + the magnitude and phase of the describing function at amplitude :math:`a`. + + Parameters + ---------- + fcn : callable + The function fcn() should accept a scalar number as an argument and + return a scalar number. For compatibility with (static) nonlinear + input/output systems, the output can also return a 1D array with a + single element. + + amp : float or array + The amplitude(s) at which the describing function should be calculated. + + Returns + ------- + df : complex or array of complex + The (complex) value of the describing fuction at the given amplitude. + + Raises + ------ + TypeError + If amp < 0 or if amp = 0 and the function fcn(0) is non-zero. + + """ + # + # The describing function of a nonlinear function F() can be computed by + # evaluating the nonlinearity over a sinusoid. The Fourier series for a + # static noninear function evaluated on a sinusoid can be written as + # + # F(a\sin\omega t) = \sum_{k=1}^\infty M_k(a) \sin(k\omega t + \phi_k(a)) + # + # The describing function is given by the complex number + # + # N(a) = M_1(a) e^{j \phi_1(a)} / a + # + # To compute this, we compute F(\theta) for \theta between 0 and 2 \pi, + # use the identities + # + # \sin(\theta + \phi) = \sin\theta \cos\phi + \cos\theta \sin\phi + # \int_0^{2\pi} \sin^2 \theta d\theta = \pi + # \int_0^{2\pi} \cos^2 \theta d\theta = \pi + # + # and then integate the product against \sin\theta and \cos\theta to obtain + # + # \int_0^{2\pi} F(a\sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi + # \int_0^{2\pi} F(a\sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi + # + # From these we can compute M1 and \phi. + # + + # Evaluate over a full range of angles + theta = np.linspace(0, 2*np.pi, num_points) + dtheta = theta[1] - theta[0] + sin_theta = np.sin(theta) + cos_theta = np.cos(theta) + + # Initialize any internal state by going through an initial cycle + [fcn(x) for x in np.atleast_1d(amp).min() * sin_theta] + + # Go through all of the amplitudes we were given + df = [] + for a in np.atleast_1d(amp): + # Make sure we got a valid argument + if a == 0: + # Check to make sure the function has zero output with zero input + if zero_check and np.squeeze(fcn(0.)) != 0: + raise ValueError("function must evaluate to zero at zero") + df.append(1.) + continue + elif a < 0: + raise ValueError("cannot evaluate describing function for amp < 0") + + # Save the scaling factor for to make the formulas simpler + scale = dtheta / np.pi / a + + # Evaluate the function (twice) along a sinusoid (for internal state) + fcn_eval = np.array([fcn(x) for x in a*sin_theta]).squeeze() + + # Compute the prjections onto sine and cosine + df_real = (fcn_eval @ sin_theta) * scale # = M_1 \cos\phi / a + df_imag = (fcn_eval @ cos_theta) * scale # = M_1 \sin\phi / a + + df.append(df_real + 1j * df_imag) + + # Return the values in the same shape as they were requested + return np.array(df).reshape(np.shape(amp)) + + +def describing_function_plot(H, F, a, omega=None): + """Plot a Nyquist plot with a describing function for a nonlinear system. + + This function generates a Nyquist plot for a closed loop system consisting + of a linear system with a static nonlinear function in the feedback path. + + Parameters + ---------- + H : LTI system + Linear time-invariant (LTI) system (state space, transfer function, or + FRD) + F : static nonlinear function + A static nonlinearity, either a scalar function or a single-input, + single-output, static input/output system. + a : list + List of amplitudes to be used for the describing function plot. + omega : list, optional + List of frequences to be used for the linear system Nyquist curve. + + """ + # Start by drawing a Nyquist curve + H_real, H_imag, H_omega = nyquist_plot(H, omega, plot=True) + + # Compute the describing function + df = describing_function(F, a) + dfinv = -1/df + + # Now add on the describing function + plt.plot(dfinv.real, dfinv.imag) + + +# Class for nonlinear functions +class NonlinearFunction(): + def sector_bounds(self, lb, ub): + raise NotImplementedError( + "sector bounds not implemented for this function") + + def describing_function(self, amp): + raise NotImplementedError( + "describing function not implemented for this function") + + # Function to compute the describing function + def _f(self, x): + return math.copysign(1, x) if abs(x) > 1 else \ + (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi + + +# Saturation nonlinearity +class saturation_nonlinearity(NonlinearFunction): + def __init__(self, ub=1, lb=None): + # Process arguments + if lb == None: + # Only received one argument; assume symmetric around zero + lb, ub = -abs(ub), abs(ub) + + # Make sure the bounds are sensity + if lb > 0 or ub < 0 or lb + ub != 0: + warn("asymmetric saturation; ignoring non-zero bias term") + + self.lb = lb + self.ub = ub + + def __call__(self, x): + return np.maximum(self.lb, np.minimum(x, self.ub)) + + def describing_function(self, A): + if self.lb <= A and A <= self.ub: + return 1. + else: + alpha, beta = math.asin(self.ub/A), math.asin(-self.lb/A) + return (math.sin(alpha + beta) * math.cos(alpha - beta) + + (alpha + beta)) / math.pi + + +# Hysteresis w/ deadzone (#40 in Gelb and Vander Velde, 1968) +class hysteresis_deadzone_nonlinearity(NonlinearFunction): + def __init__(self, delta, D, m): + # Initialize the state to bottom branch + self.branch = -1 # lower branch + self.delta = delta + self.D = D + self.m = m + + def __call__(self, x): + if x > self.delta + self.D / self.m: + y = self.m * (x - self.delta) + self.branch = 1 + elif x < -self.delta - self.D/self.m: + y = self.m * (x + self.delta) + self.branch = -1 + elif self.branch == -1 and \ + x > -self.delta - self.D / self.m and \ + x < self.delta - self.D / self.m: + y = -self.D + elif self.branch == -1 and x >= self.delta - self.D / self.m: + y = self.m * (x - self.delta) + elif self.branch == 1 and \ + x > -self.delta + self.D / self.m and \ + x < self.delta + self.D / self.m: + y = self.D + elif self.branch == 1 and x <= -self.delta + self.D / self.m: + y = self.m * (x + self.delta) + return y + + def describing_function(self, A): + def f(x): + return math.copysign(1, x) if abs(x) > 1 else \ + (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi + + if A < self.delta + self.D/self.m: + return np.nan + + df_real = self.m/2 * \ + (2 - self._f((self.D/self.m + self.delta)/A) + + self._f((self.D/self.m - self.delta)/A)) + df_imag = -4 * self.D * self.delta / (math.pi * A**2) + return df_real + 1j * df_imag + + +# Backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) +class backlash_nonlinearity(NonlinearFunction): + def __init__(self, b): + self.b = b # backlash distance + self.center = 0 # current center position + + def __call__(self, x): + # If we are outside the backlash, move and shift the center + if x - self.center > self.b/2: + self.center = x - self.b/2 + elif x - self.center < -self.b/2: + self.center = x + self.b/2 + return self.center + + def describing_function(self, A): + if A < self.b/2: + return 0 + + df_real = (1 + self._f(1 - self.b/A)) / 2 + df_imag = -(2 * self.b/A - (self.b/A)**2) / math.pi + return df_real + 1j * df_imag diff --git a/control/tests/nltools_test.py b/control/tests/nltools_test.py new file mode 100644 index 000000000..1b5755473 --- /dev/null +++ b/control/tests/nltools_test.py @@ -0,0 +1,106 @@ +"""nltools_test.py - test static nonlinear feedback functionality + +RMM, 23 Jan 2021 + +This set of unit tests covers the various operatons of the nltools module, as +well as some of the support functions associated with static nonlinearities. + +""" + +import pytest + +import numpy as np +import control as ct +import math + +class saturation(): + # Static nonlinear saturation function + def __call__(self, x, lb=-1, ub=1): + return np.maximum(lb, np.minimum(x, ub)) + + # Describing function for a saturation function + def describing_function(self, a): + if -1 <= a and a <= 1: + return 1. + else: + b = 1/a + return 2/math.pi * (math.asin(b) + b * math.sqrt(1 - b**2)) + + +# Static nonlinear system implementing saturation +@pytest.fixture +def satsys(): + satfcn = saturation() + def _satfcn(t, x, u, params): + return satfcn(u) + return ct.NonlinearIOSystem(None, outfcn=_satfcn, input=1, output=1) + + +def test_static_nonlinear_call(satsys): + # Make sure that the saturation system is a static nonlinearity + assert satsys.isstatic() + + # Make sure the saturation function is doing the right computation + input = [-2, -1, -0.5, 0, 0.5, 1, 2] + desired = [-1, -1, -0.5, 0, 0.5, 1, 1] + for x, y in zip(input, desired): + assert satsys(x) == y + + # Test squeeze properties + assert satsys(0.) == 0. + assert satsys([0.], squeeze=True) == 0. + np.testing.assert_array_equal(satsys([0.]), [0.]) + + # Test SIMO nonlinearity + def _simofcn(t, x, u, params={}): + return np.array([np.cos(u), np.sin(u)]) + simo_sys = ct.NonlinearIOSystem(None, outfcn=_simofcn, input=1, output=2) + np.testing.assert_array_equal(simo_sys([0.]), [1, 0]) + np.testing.assert_array_equal(simo_sys([0.], squeeze=True), [1, 0]) + + # Test MISO nonlinearity + def _misofcn(t, x, u, params={}): + return np.array([np.sin(u[0]) * np.cos(u[1])]) + miso_sys = ct.NonlinearIOSystem(None, outfcn=_misofcn, input=2, output=1) + np.testing.assert_array_equal(miso_sys([0, 0]), [0]) + np.testing.assert_array_equal(miso_sys([0, 0]), [0]) + np.testing.assert_array_equal(miso_sys([0, 0], squeeze=True), [0]) + + +# Test saturation describing function in multiple ways +def test_saturation_describing_function(satsys): + satfcn = saturation() + + # Store the analytic describing function for comparison + amprange = np.linspace(0, 10, 100) + df_anal = [satfcn.describing_function(a) for a in amprange] + + # Compute describing function for a static function + df_fcn = [ct.describing_function(satfcn, a) for a in amprange] + np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) + + # Compute describing function for a static I/O system + df_sys = [ct.describing_function(satsys, a) for a in amprange] + np.testing.assert_almost_equal(df_sys, df_anal, decimal=3) + + # Compute describing function on an array of values + df_arr = ct.describing_function(satsys, amprange) + np.testing.assert_almost_equal(df_arr, df_anal, decimal=3) + +from control.nltools import saturation_nonlinearity, backlash_nonlinearity, \ + hysteresis_deadzone_nonlinearity + + +@pytest.mark.parametrize("fcn, amin, amax", [ + [saturation_nonlinearity(1), 0, 10], + [backlash_nonlinearity(2), 1, 10], + [hysteresis_deadzone_nonlinearity(1, 1, 1), 3, 10], + ]) +def test_describing_function(fcn, amin, amax): + # Store the analytic describing function for comparison + amprange = np.linspace(amin, amax, 100) + df_anal = [fcn.describing_function(a) for a in amprange] + + # Compute describing function on an array of values + df_arr = ct.describing_function(fcn, amprange, zero_check=False) + np.testing.assert_almost_equal(df_arr, df_anal, decimal=1) From c85491ebfbb78b830685ba42e4b834f55f14d5af Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 27 Jan 2021 23:05:04 -0800 Subject: [PATCH 2/8] add intersection computation + Jupyter notebook, documentation --- control/freqplot.py | 19 +- control/nltools.py | 186 +++++++++---- control/tests/nltools_test.py | 43 ++- examples/describing_functions.ipynb | 401 ++++++++++++++++++++++++++++ 4 files changed, 591 insertions(+), 58 deletions(-) create mode 100644 examples/describing_functions.ipynb diff --git a/control/freqplot.py b/control/freqplot.py index ce337844a..daf73d931 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -520,9 +520,10 @@ def gen_zero_centered_series(val_min, val_max, period): # Nyquist plot # -def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, - omega_num=None, label_freq=0, arrowhead_length=0.1, - arrowhead_width=0.1, color=None, *args, **kwargs): +def nyquist_plot( + syslist, omega=None, plot=True, omega_limits=None, omega_num=None, + label_freq=0, arrowhead_length=0.1, arrowhead_width=0.1, + mirror='--', color=None, *args, **kwargs): """ Nyquist plot for a system @@ -643,11 +644,13 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None, head_width=arrowhead_width, head_length=arrowhead_length) - plt.plot(x, -y, '-', color=c, *args, **kwargs) - ax.arrow( - x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2, - fc=c, ec=c, head_width=arrowhead_width, - head_length=arrowhead_length) + if mirror is not False: + plt.plot(x, -y, mirror, color=c, *args, **kwargs) + ax.arrow( + x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2, + fc=c, ec=c, head_width=arrowhead_width, + head_length=arrowhead_length) + # Mark the -1 point plt.plot([-1], [0], 'r+') diff --git a/control/nltools.py b/control/nltools.py index 40d64cd5f..8f4562880 100644 --- a/control/nltools.py +++ b/control/nltools.py @@ -17,6 +17,7 @@ import math import numpy as np import matplotlib.pyplot as plt +import scipy from numpy import where, dstack, diff, meshgrid from warnings import warn @@ -28,7 +29,8 @@ def sector_bounds(fcn): raise NotImplementedError("function not currently implemented") -def describing_function(fcn, amp, num_points=100, zero_check=True): +def describing_function( + fcn, amp, num_points=100, zero_check=True, try_method=True): """Numerical compute the describing function of a nonlinear function The describing function of a static nonlinear function is given by @@ -47,6 +49,18 @@ def describing_function(fcn, amp, num_points=100, zero_check=True): amp : float or array The amplitude(s) at which the describing function should be calculated. + zero_check : bool, optional + If `True` (default) then `amp` is zero, the function will be evaluated + and checked to make sure it is zero. If not, a `TypeError` exception + is raised. If zero_check is `False`, no check is made on the value of + the function at zero. + + try_method : bool, optional + If `True` (default), check the `fcn` argument to see if it is an + object with a `describing_function` method and use this to compute the + describing function. See the :class:`NonlienarFunction` class for + more information on the `describing_function` method. + Returns ------- df : complex or array of complex @@ -58,6 +72,14 @@ def describing_function(fcn, amp, num_points=100, zero_check=True): If amp < 0 or if amp = 0 and the function fcn(0) is non-zero. """ + # If there is an analytical solution, trying using that first + if try_method and hasattr(fcn, 'describing_function'): + # Go through all of the amplitudes we were given + df = [] + for a in np.atleast_1d(amp): + df.append(fcn.describing_function(a)) + return np.array(df).reshape(np.shape(amp)) + # # The describing function of a nonlinear function F() can be computed by # evaluating the nonlinearity over a sinusoid. The Fourier series for a @@ -83,7 +105,7 @@ def describing_function(fcn, amp, num_points=100, zero_check=True): # # From these we can compute M1 and \phi. # - + # Evaluate over a full range of angles theta = np.linspace(0, 2*np.pi, num_points) dtheta = theta[1] - theta[0] @@ -122,7 +144,8 @@ def describing_function(fcn, amp, num_points=100, zero_check=True): return np.array(df).reshape(np.shape(amp)) -def describing_function_plot(H, F, a, omega=None): +def describing_function_plot( + H, F, a, omega=None, refine=True, label="%5.2g @ %-5.2g", **kwargs): """Plot a Nyquist plot with a describing function for a nonlinear system. This function generates a Nyquist plot for a closed loop system consisting @@ -133,7 +156,7 @@ def describing_function_plot(H, F, a, omega=None): H : LTI system Linear time-invariant (LTI) system (state space, transfer function, or FRD) - F : static nonlinear function + F : static nonlinear function A static nonlinearity, either a scalar function or a single-input, single-output, static input/output system. a : list @@ -141,16 +164,90 @@ def describing_function_plot(H, F, a, omega=None): omega : list, optional List of frequences to be used for the linear system Nyquist curve. + Returns + ------- + intersection_list : 1D array of 2-tuples + A list of all amplitudes and frequencies in which :math:`H(j\\omega) + N(a) = -1`, where :math:`N(a)` is the describing function associated + with `F`, or `None` if there are no such points. Each pair represents + a potential limit cycle for the closed loop system. + """ # Start by drawing a Nyquist curve - H_real, H_imag, H_omega = nyquist_plot(H, omega, plot=True) + H_real, H_imag, H_omega = nyquist_plot(H, omega, plot=True, **kwargs) + H_vals = H_real + 1j * H_imag # Compute the describing function df = describing_function(F, a) - dfinv = -1/df - - # Now add on the describing function - plt.plot(dfinv.real, dfinv.imag) + N_vals = -1/df + + # Now add the describing function curve to the plot + plt.plot(N_vals.real, N_vals.imag) + + # Look for intersection points + intersections = [] + for i in range(N_vals.size - 1): + for j in range(H_vals.size - 1): + intersect = _find_intersection( + N_vals[i], N_vals[i+1], H_vals[j], H_vals[j+1]) + if intersect == None: + continue + + # Found an intersection, compute a and omega + s_amp, s_omega = intersect + a_guess = (1 - s_amp) * a[i] + s_amp * a[i+1] + omega_guess = (1 - s_omega) * H_omega[j] + s_omega * H_omega[j+1] + + # Refine the coarse estimate to get better intersection point + a_final, omega_final = a_guess, omega_guess + if refine: + # Refine the answer to get more accuracy + def _cost(x): + return abs(1 + H(1j * x[1]) * + describing_function(F, x[0]))**2 + res = scipy.optimize.minimize(_cost, [a_guess, omega_guess]) + + if not res.success: + warn("not able to refine result; returning estimate") + else: + a_final, omega_final = res.x[0], res.x[1] + + # Add labels to the intersection points + if label: + pos = H(1j * omega_final) + plt.text(pos.real, pos.imag, label % (a_final, omega_final)) + + # Save the final estimate + intersections.append((a_final, omega_final)) + + return intersections + +# Figure out whether two line segments intersection +def _find_intersection(L1a, L1b, L2a, L2b): + # Compute the tangents for the segments + L1t = L1b - L1a + L2t = L2b - L2a + + # Set up components of the solution: b = M s + b = L1a - L2a + detM = L1t.imag * L2t.real - L1t.real * L2t.imag + if abs(detM) < 1e-8: # TODO: fix magic number + return None + + # Solve for the intersection points on each line segment + s1 = (L2t.imag * b.real - L2t.real * b.imag) / detM + if s1 < 0 or s1 > 1: + return None + + s2 = (L1t.imag * b.real - L1t.real * b.imag) / detM + if s2 < 0 or s2 > 1: + return None + + # Debugging test + np.testing.assert_almost_equal(L1a + s1 * L1t, L2a + s2 * L2t) + + # Intersection is within segments; return proportional distance + return (s1, s2) # Class for nonlinear functions @@ -195,49 +292,38 @@ def describing_function(self, A): return (math.sin(alpha + beta) * math.cos(alpha - beta) + (alpha + beta)) / math.pi - -# Hysteresis w/ deadzone (#40 in Gelb and Vander Velde, 1968) -class hysteresis_deadzone_nonlinearity(NonlinearFunction): - def __init__(self, delta, D, m): + +# Relay with hysteresis (FBS2e, Example 10.12) +class relay_hysteresis_nonlinearity(NonlinearFunction): + def __init__(self, b, c): # Initialize the state to bottom branch self.branch = -1 # lower branch - self.delta = delta - self.D = D - self.m = m + self.b = b + self.c = c def __call__(self, x): - if x > self.delta + self.D / self.m: - y = self.m * (x - self.delta) + if x > self.c: + y = self.b self.branch = 1 - elif x < -self.delta - self.D/self.m: - y = self.m * (x + self.delta) + elif x < -self.c: + y = -self.b self.branch = -1 - elif self.branch == -1 and \ - x > -self.delta - self.D / self.m and \ - x < self.delta - self.D / self.m: - y = -self.D - elif self.branch == -1 and x >= self.delta - self.D / self.m: - y = self.m * (x - self.delta) - elif self.branch == 1 and \ - x > -self.delta + self.D / self.m and \ - x < self.delta + self.D / self.m: - y = self.D - elif self.branch == 1 and x <= -self.delta + self.D / self.m: - y = self.m * (x + self.delta) + elif self.branch == -1: + y = -self.b + elif self.branch == 1: + y = self.b return y - def describing_function(self, A): + def describing_function(self, a): def f(x): return math.copysign(1, x) if abs(x) > 1 else \ (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi - if A < self.delta + self.D/self.m: + if a < self.c: return np.nan - - df_real = self.m/2 * \ - (2 - self._f((self.D/self.m + self.delta)/A) + - self._f((self.D/self.m - self.delta)/A)) - df_imag = -4 * self.D * self.delta / (math.pi * A**2) + + df_real = 4 * self.b * math.sqrt(1 - (self.c/a)**2) / (a * math.pi) + df_imag = -4 * self.b * self.c / (math.pi * a**2) return df_real + 1j * df_imag @@ -248,17 +334,23 @@ def __init__(self, b): self.center = 0 # current center position def __call__(self, x): - # If we are outside the backlash, move and shift the center - if x - self.center > self.b/2: - self.center = x - self.b/2 - elif x - self.center < -self.b/2: - self.center = x + self.b/2 - return self.center + # Convert input to an array + x_array = np.array(x) + + y = [] + for x in np.atleast_1d(x_array): + # If we are outside the backlash, move and shift the center + if x - self.center > self.b/2: + self.center = x - self.b/2 + elif x - self.center < -self.b/2: + self.center = x + self.b/2 + y.append(self.center) + return(np.array(y).reshape(x_array.shape)) def describing_function(self, A): - if A < self.b/2: + if A <= self.b/2: return 0 - + df_real = (1 + self._f(1 - self.b/A)) / 2 df_imag = -(2 * self.b/A - (self.b/A)**2) / math.pi return df_real + 1j * df_imag diff --git a/control/tests/nltools_test.py b/control/tests/nltools_test.py index 1b5755473..074feae84 100644 --- a/control/tests/nltools_test.py +++ b/control/tests/nltools_test.py @@ -88,13 +88,13 @@ def test_saturation_describing_function(satsys): np.testing.assert_almost_equal(df_arr, df_anal, decimal=3) from control.nltools import saturation_nonlinearity, backlash_nonlinearity, \ - hysteresis_deadzone_nonlinearity + relay_hysteresis_nonlinearity @pytest.mark.parametrize("fcn, amin, amax", [ [saturation_nonlinearity(1), 0, 10], [backlash_nonlinearity(2), 1, 10], - [hysteresis_deadzone_nonlinearity(1, 1, 1), 3, 10], + [relay_hysteresis_nonlinearity(1, 1), 3, 10], ]) def test_describing_function(fcn, amin, amax): # Store the analytic describing function for comparison @@ -102,5 +102,42 @@ def test_describing_function(fcn, amin, amax): df_anal = [fcn.describing_function(a) for a in amprange] # Compute describing function on an array of values - df_arr = ct.describing_function(fcn, amprange, zero_check=False) + df_arr = ct.describing_function( + fcn, amprange, zero_check=False, try_method=False) np.testing.assert_almost_equal(df_arr, df_anal, decimal=1) + + # Make sure the describing function method also works + df_meth = ct.describing_function(fcn, amprange, zero_check=False) + np.testing.assert_almost_equal(df_meth, df_anal, decimal=1) + +def test_describing_function_plot(): + # Simple linear system with at most 1 intersection + H_simple = ct.tf([1], [1, 2, 2, 1]) + omega = np.logspace(-1, 2, 100) + + # Saturation nonlinearity + F_saturation = ct.nltools.saturation_nonlinearity(1) + amp = np.linspace(1, 4, 10) + + # No intersection + xsects = ct.describing_function_plot(H_simple, F_saturation, amp, omega) + assert xsects == [] + + # One intersection + H_larger = H_simple * 8 + xsects = ct.describing_function_plot(H_larger, F_saturation, amp, omega) + for a, w in xsects: + np.testing.assert_almost_equal( + H_larger(1j*w), + -1/ct.describing_function(F_saturation, a), decimal=5) + + # Multiple intersections + H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4 + omega = np.logspace(-1, 3, 50) + F_backlash = ct.nltools.backlash_nonlinearity(1) + amp = np.linspace(0.6, 5, 50) + xsects = ct.describing_function_plot(H_multiple, F_backlash, amp, omega) + for a, w in xsects: + np.testing.assert_almost_equal( + -1/ct.describing_function(F_backlash, a), + H_multiple(1j*w), decimal=5) diff --git a/examples/describing_functions.ipynb b/examples/describing_functions.ipynb new file mode 100644 index 000000000..d46ecba95 --- /dev/null +++ b/examples/describing_functions.ipynb @@ -0,0 +1,401 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Describing Function Analysis Using the Python Control Toolbox (python-control)\n", + "### Richard M. Murray, 27 Jan 2021\n", + "This Jupyter notebook shows how to use the `nltools` module of the Python Control Toolbox to perform describing function analysis of a nonlinear system. A brief introduction to describing functions can be found in [Feedback Systems](https://fbsbook.org), Section 10.5 (Generalized Notions of Gain and Phase)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import control as ct\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import math" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in describing functions\n", + "The Python Control Toobox has a number of built-in functions that provide describing functions for some standard nonlinearities. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Saturation nonlinearity\n", + "\n", + "A saturation nonlinearity can be obtained using the `ct.nltools.saturation_nonlinearity` function. This function takes the saturation level as an argument." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "saturation=ct.nltools.saturation_nonlinearity(0.75)\n", + "x = np.linspace(-2, 2, 50)\n", + "plt.plot(x, saturation(x))\n", + "plt.xlabel(\"Input, x\")\n", + "plt.ylabel(\"Output, y = sat(x)\")\n", + "plt.title(\"Input/output map for a saturation nonlinearity\");" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "amp_range = np.linspace(0, 2, 50)\n", + "plt.plot(amp_range, ct.describing_function(saturation, amp_range))\n", + "plt.xlabel(\"Amplitude A\")\n", + "plt.ylabel(\"Describing function, N(A)\")\n", + "plt.title(\"Describing function for a saturation nonlinearity\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Backlash nonlinearity\n", + "A backlash nonlinearity can be obtained using the `ct.nltools.backlash_nonlinearity` function. This function takes as is argument the size of the backlash region." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "backlash = ct.nltools.backlash_nonlinearity(0.5)\n", + "theta = np.linspace(0, 2*np.pi, 50)\n", + "x = np.sin(theta)\n", + "plt.plot(x, backlash(x))\n", + "plt.xlabel(\"Input, x\")\n", + "plt.ylabel(\"Output, y = backlash(x)\")\n", + "plt.title(\"Input/output map for a backlash nonlinearity\");" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAA0W0lEQVR4nO3deXwddb3/8de7adO0Wdp03ze6UaBAW/Z9UVkFF5aCC4ggKq5Xxfu7XuAqXldUFLEisnhBUFlkERGRfactlNJ9b9Okbdo0TdI0zfb5/TETPKTJySTNnJPkfJ6Px3nkzP7JnDnzOfP9zny/MjOcc85lrl7pDsA551x6eSJwzrkM54nAOecynCcC55zLcJ4InHMuw3kicM65DOeJoJuQdKmkpxKGTdLkKPN2chzTJL0lqVLSl+PYRivbHSepSlJWDOs+TtKqcP3nd/b629j2yZKKYljvXZJubO+0dqz/Mkkv7c86OkPzOMLPcFIa4ojt+EwFTwRtkLRe0p7wxFcu6RVJV0tK6b4zs3vN7IOdPW8HfAt4zszyzeyXMW2jab+f3jRsZhvNLM/MGmLY3HeBW8L1/zWG9bsUCT/DtWnY7vuOT0nPSfpsquPoKE8E0ZxrZvnAeOCHwLXA71O1cUm9U7WtCMYDS9IdRCfr8P/UxT4blwY94RjwRNAOZrbLzB4FLgI+LelgAEl9Jf1U0kZJWyXNk9QvnDZE0uPh1USZpBebriYkjZX0kKRSSTsk3RKOv0zSy5J+LqkMuKGVS/GzJK2VtF3STxLW2/xy2cKrmFWSdkr6tSSF07Ik3RSuY52ka8L59zm4JT0DnALcEl4GT23+y6c92w6nXylpWXjFtVTSLEn/B4wDHgu38y1JExLjkjRK0qPhPl0t6cqEdd4g6c+S/hCud4mkOS19ppLWAJMSttU3wrofkHSPpArgshbWebaC4rMKSZsk3dDStpst8//Cz2C9pEujrkvS8eFVank4vaV48iU9K+mXifs+nFYYHp+l4efzuKQxCdMvC4+xyvD4uLTZ8j8Nl1sn6cwk/996Sd+Q9I6kXZL+JCknYfqV4b4uC/f9qIRpSY+hZtt5r8hUQRHYryX9LYz/dUkHJMw7XdI/w22ukHRhlP2ecCxeIWkj8Ezi8Snp+8AJ/Pt7cksYx03NYn1M0ldb22cpZWb+SvIC1gOntzB+I/D58P0vgEeBQUA+8Bjwg3DaD4B5QJ/wdQIgIAtYBPwcyAVygOPDZS4D6oEvAb2BfuG4lxK2b8Cz4TbHASuBzyYs33zex4GB4bylwBnhtKuBpcAYoBB4Opy/dyv747mm7bQy3J5tXwBsBo4I98lkYHxL+x2YkBgX8Dxwa7jfDgvXe1o47QagBjgr3M8/AF6L+hlHWHcdcD7BD6l+LazvZOCQcPpMYCtwfivbPjn8rH8G9AVOAnYD09paV7g/K4G5BMfWYOCwcNpdwI3huDeAGxO2eVfTcDj9Y0B/gmP3L8Bfw2m5QEVCLCOBgxI+5zrgynAffx4oBpRkH78BjCI4ZpcBV4fTTgW2A7PCffAr4IWIx9Bl7Hu8TU74P8uAIwm+R/cC9yf8b5uAy8Nps8IYDoqw3yeE2/lDuJ5+7Ht8Psf7vxdHhvunVzg8BKgGhqf7HGdmfkWwH4qBQeEvkyuBr5lZmZlVAv8LXBzOV0fwBRpvZnVm9qIFR8KRBF+Kb5rZbjOrMbPEX/zFZvYrM6s3sz2txPCjcJsbCZLR3CTx/tDMysN5nyU4wQFcCNxsZkVmtpOg6KuztbbtzwI/NrM3LbDazDa0tTJJY4HjgWvD/fY2cDvwyYTZXjKzJywos/0/4NAogUZc96tm9lcza2zpszGz58xscTj9HeA+ghN8Mv9tZnvN7HngbwSfS1vruhR42szuC4+tHWG8TUYRJLW/mNl3WtpouMyDZlYdHrvfbxZrI3CwpH5mVmJmiUVoG8zsd+E+vpvgOB+e5H/8pZkVm1kZwY+lwxL+jzvMbKGZ7QX+EzhG0oSEZVs7htrykJm9YWb1BImgablzgPVmdmf4HVsIPAh8PNwvUT7DG8Lvbmvfz/eY2RvALuC0cNTFBHVtWyP+H7HyRNBxowl+bQwl+DW1ILw8LweeDMcD/ARYDTwVXmJ/Oxw/luCLVN/K+jdFiCFxng0EX/zWbEl4Xw3khe9HNVtPlO22V2vbHgus6cD6RgFNSbfJBoLPpLVt5ihaWW6UdSfdR5KOCotiSiXtIrjqGpJkkZ1mtrvZ9kZFWFdb++9sgl+r85LE2l/SbyVtCIu6XgAGSsoKY7oo3GZJWMQyPWHx9/axmVWHb/NoXbJj8L0fAGZWBewg+eeZbDtRtjkeOKrpOxt+by8FRkDkz7C935W7gU+E7z9B8AOlS/BE0AGSjiA4SF8iuJzcQ3BJOTB8DTCzPAAzqzSz/zCzScC5wNclnUZwEI1LcnKK0izs2IT34wiuUtqrhKBYqKV1RrGbIBE2GdGOZTcBB7QyLdn/33Q1lp8wbhxBMdP+irLutj6bPxIUFY41swEEJ+IWy7RDhZJym22v6bNMtq5k+w/gdwQ/Sp5otv5E/wFMA44yswLgxHC8AMzsH2b2AYJf+8vDdXa2YoITc7DhINbBdM7n2ZpNwPMJ39mBFtz18/lwepTPMNlx0NK0e4DzJB0KHAj8db/+g07kiaAdJBVIOge4H7in6dKR4Mvxc0nDwvlGS/pQ+P4cSZPDIqQKoCF8vUFwEv6hpFxJOZKOa2dI3wwr+8YCXwH+1IF/68/AV8KYBxLcEdUebwMfDX9ZTgauaMeytwPfkDRbgcmSmk4IWwkqcfdhZpuAV4AfhPttZrjde9sZe1zrzie4qqiRdCRwSYRl/kdStqQTCIot/hJhXfcCp0u6MKykHCzpsGbrvQZYATyu8AaGFmLdA5RLGgRc3zRB0nBJHw5PzHuBKoJjt7P9Ebhc0mGS+hIUrb5uZutj2FaTx4Gpkj4pqU/4OkLSgeH0jnyGifY5fs2sCHiT4ErgwShFSqniiSCaxyRVEvyK+C+Cir3LE6ZfS1D881p4ef00wa8sgCnhcBXwKnBrWP7YQHCFMJmg4rmI4DK8PR4BFhCcjP9Gx25p/R3wFPAO8BbwBEHlZdQv/M+BWoID/27accI0s78QlEn/kaDS868EFYkQVPB+J7xs/0YLi88lqKArBh4Grjezf0bddhv2d91fAL4bHjPXESTbZLYAO8Pt3UtQibq8rXWF5eVnEfyqLyM4Dt5XFxLWR11FcOw+ooQ7dUK/ICg+2g68RnAF0aRXuO7icP0nhfF0KjP7F/DfBGX0JQRXORcnXWj/t1kJfDDcTjHBZ/AjgspqaP9n2NzNwMcV3OWU+LzN3QSV0F2mWAjCGn7nmii4BXCemY1vc2bnXLtIOpGgiGhCWJrQJfgVQYaT1E/SWWHRwmiCooGH0x2Xcz2NpD4ERbi3d6UkAJ4IXFAB9j8ERRNvEdzffV1aI3KuhwnrHsoJKt1/kdZgWuBFQ845l+H8isA55zJct2ssaciQITZhwoR0h+Gcc93KggULtpvZ0JamdbtEMGHCBObPn5/uMJxzrluR1GrzLV405JxzGc4TgXPOZThPBM45l+E8ETjnXIbzROCccxnOE4FzzmU4TwTOOZfhut1zBM451xM1NhqVe+up2FNHRU0dFXvqqaypo6ImGFdZU8+s8QM5YUqLz4TtF08EzjnXScyM3bUNlFfXUl5dF7z21LJrT13wqq779/vwVVETjK/cW09bTb99/uQDPBE451yqNDYalTX1lFXXUrZ7L2W769i5u5ay6lp2VtdSvrsu+FtdR1n4d9eeWuoaWj+bZ/fuxYB+fd57DS/IYerwfApyejOgXx8Kml45fSjo1zv4G77P69ub3lnxlOZ7InDOZQQzo2pvPduratlRtZftVXvD98GJfvvuWsqqainbXcuO3XvZWV1HQ2PLJ/XsrF4M7N+Hwv7ZDOzfh8lD8yjMDd4P7BeMHxC+HxjOM6BfH3L6ZKX4v47GE4FzrlvbW9/Atoq9bKvcS2nlXkqrwr8Jw9srgxP/3vqW+4MpyOnN4Ly+DM7NZvzg/swaP5BBudkU9s9mcF7wt2l4UG42/bOzCLoh7xk8ETjnuqSGRmNH1V62VNRQsquGrRU1bNlVw9aKvWyrrGFbxV62VtZQXl23z7ISDM7ty9D84HXA0FyG5vVlcF42Q/L6MjivL0Pyshmc25dBudlk987sGyg9ETjnUs7MKNtdy+byPRSX76G4vIYtFTUUl++hZFcNJeV72Fq5d5+imaxeYlh+X4YX5DB+cH+OmFjI8PwchhX0ZVj4d2h+Xwb1z46tPL0nSpoIJB0DfAI4gaCLtT3Au8DfgHvMbFfsETrnuh0zo7RyL5t27qFoZzVFCX+bTv41de8vpsnu3YtRA3IYMSCHoycNZuTAHEYU5DC8IBg3oiCHwXl9yerVc4pkuopWE4GkvwPFwCPA94FtQA4wFTgFeETSz8zs0VQE6pzrWmrqGthYVs2GHdVsLKtm447dbCirZlNZcMJvXh4/ODeb0YX9mDY8n1OnDWPUwH6MLuzH6IH9GDkgh0G52T2q3L07SXZF8Ekz295sXBWwMHzdJGlIbJE559Jub30Dm8qqWVu6m/U7drNue/Bav72aLRU175s3r29vxg3qz5Rh+Zw6fRhjB/VnTGE/xhb2Z0xhf/pld807ZlySRNBCEgBA0nHAJWb2xdbmcc51Lzt317KmtIo1pVWs3lbFmtLdrCmtYlNZNYnF9IX9+zBxSC7HTh7M+EG5jB/cn3GD+zN+UH//Rd+NRaoslnQYcAlwIbAOeCjGmJxzMdlVXcfKbZWs3FrJyi2VrNxaxaptlWyvqn1vnuzevZg0JJeDRw/gvENHMXFoLhOH5DFxcC4D+vdJY/QuLsnqCKYCFwNzgR3AnwCZ2Skpis0510H1DY2s37GbpSWVLCupYHlJBctKKt9XnJObncWU4UExzpRh+UwelscBQ/MYXdjPK2QzTLIrguXAi8C5ZrYaQNLXUhKVcy6ymroGVm6tZPHmXby7uYIlxbtYsaXyvcra3r3E5GF5HHPAYKaNyGfa8HymDM9j9MB+XpTjgOSJ4GMEVwTPSnoSuB/wo8a5NKpvaGT5lkoWFZWzaFM5izdXsGprJfVhQf6Afn04aFQBnzpmPAeOLGD6iAImD8vL+AemXHLJKosfBh6WlAucD3wNGC7pN8DDZvZUakJ0LnOV7NrDgg07WbSpnLc3lbN486737r8v7N+Hg0cP4JRpkzhk9AAOHj2AMYX+K9+1X5uVxWa2G7gXuFfSIOAC4NuAJwLnOlFDo7FiSyULNpQxf8NO5q/fyebyPUBQgXvwqAIuOXI8h44dwOFjCxk7yE/6rnMkqyzOM7OqxHFmVgb8Nny1OI9zLpqGRmNJ8S5eXbOD19buYP76nVTurQdgeEFf5owfxGdPmMjs8YVMH1HgxTsuNsmuCB6R9DbBk8ULwisDJE0CTgYuAn4HPBBzjM71CGbGspJKXlmznVfX7OCNdWXvnfgPGJrLuYeN4ogJhcwZP8iLeFxKJasjOE3SWcDngOPCYqE6YAVBW0OfNrMtqQnTue5pe9VeXlxVyosrt/PCqu1sr9oLwMQhuZxz6CiOOWAwR08cxLCCnDRH6jJZ0joCM3sCeCJFsTjX7TU0Gm9vKudfy7by/MpSlhRXADAoN5vjJw/hxKlDOX7yEEYM8BO/6zqS1RGMS7agmW3s/HCc636qa+t5cdV2nl66lWeWb2PH7lqyeonZ4wv55oemccKUIRw8agC9/CEt10UluyL4G2C8/9kBA4YCwwBvQcplrF3Vdfxj6Rb+vriEl9fsoLa+kfyc3pwybRinzxjOSVOHMqCfN8fguodkdQSHJA5LmgBcC5wO/G+8YTnX9TSd/J9YXMJLq7ZT32iMKezHJ44az+kHDuOIiYPo452huG6ozecIJE0B/gs4CrgJ+LKZ7ds3nHM9UHVtPU8t2cojb2/mpdXbqWsITv5XHD+Rs2eO5JDRA/zuHtftJasjOJggARwE/Bi4wswaUhWYc+nS2Gi8vq6MhxYW8cTiEnbXNjB6YD8+c9xEzjpkJDPH+Mnf9SzJrggWAZsI6gqOBI5MPPjN7MvxhuZcam3cUc0DCzbx0FubKdq5h7y+vTl75kg+NmsMR0wY5JW9rsdKlgg+s78rl3QGcDNBxfLtZvbDZtMHAPcA48JYfmpmd+7vdp2Lqr6hkWeWb+Oe1zfywspSegmOnzKUb35oGh+cMcJ71XIZIVll8d37s2JJWcCvgQ8ARcCbkh41s6UJs30RWGpm50oaCqyQdK+Z1bawSuc6zbaKGu5/cxP3vbGRkl01jCjI4aunT+HiI8b5Pf4u40TqoayDjgRWm9laAEn3A+cBiYnAgHwFZU55QBlQH2NMLsMt2lTO715cy5PvbqG+0ThhyhCuP/cgTj9wGL39jh+XoeJMBKMJ6hiaFBHceZToFuBRoBjIBy4ys8bmK5J0FXAVwLhxSZ9zc24fZsZzK0r57QtreG1tGfk5vbns2AlcevR4Jg7JTXd4zqVdnImgpZo1azb8IeBt4FTgAOCfkl40s4r3LWR2G3AbwJw5c5qvw7kW1dY38uiiYm57YQ0rt1YxckAO3zn7QC46Yiz5Of6wl3NN2p0IJH2BoA/jB80sWTFOETA2YXgMwS//RJcDPzQzA1ZLWgdMB95ob1zONamtb+RP8zdx67OrKdlVw7Th+fzswkM5Z+Yob8rZuRZ05IpAwPHApcCHk8z3JjBF0kRgM0G3l5c0m2cjcBrwoqThwDRgbQdico76hkYeemszNz+9is3le5g9vpAffPQQTpo61O/7dy6JdicCM/t1xPnqJV0D/IPg9tE7zGyJpKvD6fOA7wF3SVpMkGCuNbPt7Y3JZbaGRuPxd4r5xdOrWLd9NzPHDOD7HznYE4BzEUVpYqIvQUf2ExLnN7PvtrVsS81Yhwmg6X0x8MHo4Tr3fs+u2MYPnljGyq1VTB+Rz22fnM0HZgz3BOBcO0S5IngE2AUsAPbGG45z0awtreJ7jy/l2RWlTBySyy2XHM5ZB4/0p3+d64AoiWCMmZ0ReyTORVBRU8ev/rWKu15ZT9/eWfzXWQfy6WMneCWwc/shSiJ4RdIhZrY49mica0Vjo/HAgiJ+/I/l7NhdywWzx/DND01naH7fdIfmXLcXJREcD1wW3tq5l6BS18xsZqyRORdaU1rFtx54hwUbdjJ7fCF3XHYEM8cMTHdYzvUYURLBmbFH4VwL6hsauf2ldfzsnyvp1yeLn15wKB+bNdorgp3rZG0mAjPbIOlQ4IRw1ItmtijesFymW7m1km/+ZRGLinbxwRnDufH8gxlW4I3BOReHKLePfgW4EngoHHWPpNvM7FexRuYyUl1DI799fg2//Ndq8nJ686u5h3POzJF+FeBcjKIUDV0BHGVmuwEk/Qh4FfBE4DrVprJqrrnvLRZtKufsmSP57ocPYnCeVwY7F7coiUBAYheVDbTcoJxzHfbUki184y+LMODWS2dx1iEj0x2ScxkjSiK4E3hd0sPh8PnA72OLyGWUuoZGfvT35dz+0joOGT2AX18yi3GD+6c7LOcySpTK4p9Jeo7gNlIBl5vZW3EH5nq+zeV7uOaPC3lrYzmfOmY8/3X2gfTt7V1DOpdqrSYCSQVmViFpELA+fDVNG2RmZfGH53qqZ5dv42t/fpv6BuOWSw7nnJmj0h2Scxkr2RXBH4FzCNoYSuwMRuHwpBjjcj3Y719ax41/W8r0EQXceuks7yXMuTRL1nn9OeHfiakLx/VkDY3GjX9byp0vr+dDBw3nFxcdTr9sLwpyLt3abKlL0r+ijHMumT21DXzh3gXc+fJ6PnPcRG69dLYnAee6iGR1BDlAf2CIpEL+fctoAeAFui6y7VV7+ezd81lUVM5158zgM8f7RaZzXUmyOoLPAV8lOOkv4N+JoAKI1EuZc2tKq7j8zjfZVlnDvE/M5kMHjUh3SM65ZpLVEdwM3CzpS96chOuIdzfv4hO/f50sifuuPJrDxxWmOyTnXAui9ObRKGlg04CkQklfiC8k1xMsKd7Fpbe/Tm52bx76wrGeBJzrwqIkgivNrLxpwMx2EjRC51yLlpVU8InbXyc3O4v7rzqa8YP99lDnurIoiaCXEpp+lJQFZMcXkuvOVmyp5NLbX6dv7yzuu+poxg7y5iKc6+qitDX0D+DPkuYRPEh2NfBkrFG5bmn1tkouvf01+mSJ+/xKwLluI0oiuJbgDqLPE9w59BRwe5xBue5nTWkVc3/3OpL445VH+9PCznUjURqdawR+E76c28eGHbuZe9trmMH9Vx3FAUPz0h2Sc64dovRQdhxwAzA+nL+p83pva8ixa08dn7nrTWobGvnz545h8rD8dIfknGunKEVDvwe+RvBQWUMb87oMUt/QyDV/XMjGsmruueIopg73JOBcdxQlEewys7/HHonrdr73+FJeXLWdH39sJkdNGpzucJxzHRQlETwr6ScEndfvbRppZgtji8p1ef/36nrufnUDV504iQuPGJvucJxz+yFKIjgq/DsnYZwBp3Z+OK47eGnVdm54bCmnTR/GtWdMT3c4zrn9FOWuoVNSEYjrHtaUVvGFexcwZVgeN889nKxeansh51yXFuWuoetaGm9m3+38cFxXVl5dyxV3vUl2717c/uk55PWNckHpnOvqonyTdye8zyHovnJZPOG4rsrM+PqfF1FcXsN9Vx3FmEJvOsK5niJK0dBNicOSfgo8GltErku69/WNPLN8GzecO4PZ4welOxznXCeK0uhcc/2J2HG9pDMkrZC0WtK3W5nnZElvS1oi6fkOxONitqa0ihv/tpSTpg7l08dOSHc4zrlOFqWOYDHBXUIAWcBQoM36gbCV0l8DHwCKgDclPWpmSxPmGQjcCpxhZhslDWv3f+BiVdfQyFfvf5t+fbL4ycdnktAQrXOuh0jWZ/FEM1tHUCfQpB7Yamb1EdZ9JLDazNaG67sfOA9YmjDPJcBDZrYRwMy2tTN+F7Obn17F4s27mPeJWQwryEl3OM65GCQrGnog/HuHmW0IX5sjJgGA0cCmhOGicFyiqUChpOckLZD0qYjrdinw5voybn1uNRfOGcMZB49MdzjOuZgkKxrqJel6YKqkrzefaGY/a2PdLZUhWLPh3sBs4DSgH/CqpNfMbOX7ViRdBVwFMG7cuDY26zpDZU0dX/vT24wp7M915x6U7nCcczFKdkVwMVBDcLLOb+HVliIgse2BMUBxC/M8aWa7zWw78AJwaPMVmdltZjbHzOYMHTo0wqbd/rrh0aUUl+/h5xcd5s8LONfDtfoNN7MVwI8kvdPBRufeBKZImghsJkgslzSb5xHgFkm9Cbq/PAr4eQe25TrRE4tLeHBhEV8+dTKzx3un8871dFGeI+hQy6NmVi/pGoKuLrMI6hqWSLo6nD7PzJZJehJ4B2gEbjezdzuyPdc5KmrquO6Rd5k5ZgBfOm1KusNxzqVArNf8ZvYE8ESzcfOaDf8E+Emccbjofvn0KnbsruWuy4+kT1ZHHjNxznU3/k1371m9rYq7XlnPRXPGcvDoAekOxzmXIlEeKPtoC6N3AYv9vv+ew8z43uNL6ZedxTc+NC3d4TjnUihK0dAVwDHAs+HwycBrBLeVftfM/i+m2FwKPbN8G8+vLOU7Zx/IkLy+6Q7HOZdCURJBI3CgmW0FkDQc+A3BHT4vAJ4Iurna+ka+9/hSJg3N5VPHTEh3OM65FItSRzChKQmEtgFTzawMqIsnLJdKd768jvU7qrnunBlk9/ZqI+cyTZQrghclPQ78JRz+GPCCpFygPK7AXGpsq6zhV8+s5rTpwzh5mrf551wmipIIvkhw8j+OoNmIPwAPmpkB3o1lN/fjJ1ewt76B75wzI92hOOfSJMoDZUbQAN0Dbc3rupe3N5XzwIIiPnfSJCYOyU13OM65NGmzQFjSRyWtkrRLUoWkSkkVqQjOxcfM+J/HljA0vy9fOtWfIHYuk0WpGfwx8GEzG2BmBWaWb2YFcQfm4vX8ylLe2ljOf3xgqjcq51yGi5IItpqZd1bfw9z63BpGDsjho7PGpDsU51yaRfkpOF/Sn4C/AnubRprZQ3EF5eI1f30Zb6wr89tFnXNAtERQAFQDH0wYZ4Angm7q1ufWUNi/DxcfObbtmZ1zPV6Uu4YuT0UgLjWWlVTwzPJtfP0DU+mf7XUDzrnkndd/y8x+LOlX7NvFJGb25Vgjc7H4zXNryM3O4tPelIRzLpTsJ2FTBfH8VATi4rdhx24ef6eYK0+YxID+fdIdjnOui0jWVeVj4d+7ASQVBINWmaLYXCf77Qtr6Z3ViyuOn5juUJxzXUiUB8rmSFpM0J3ku5IWSZodf2iuM22rqOGB+UV8fPYYhhXkpDsc51wXEqW28A7gC2b2IoCk44E7gZlxBuY61+9fWkd9YyOfO3FSukNxznUxUW4ir2xKAgBm9hLgxUPdyK7qOu55bQPnHjqK8YO9TSHn3Pslu2toVvj2DUm/Be4juHvoIuC5+ENzneXuV9ezu7aBz598QLpDcc51QcmKhm5qNnx9wvt9bid1XVN1bT13vryO06YPY/oIbyLKObevZHcNeV8DPcDj75Sws7qOz53kVwPOuZYlKxr6hJndI+nrLU03s5/FF5brLA8sKGLS0FyOmFCY7lCcc11UssriplrF/FZerovbsGM3b6wr4+OzxyAp3eE457qoZEVDv5WUBVSY2c9TGJPrJA8uKKKX4KOHe1PTzrnWJb191MwagA+nKBbXiRobjQcXbuaEKUMZMcAfIHPOtS7KA2WvSLoF+BOwu2mkmS2MLSq3315Zs4PN5Xv49pnT0x2Kc66Li5IIjg3/fjdhnAGndn44rrM8sGATBTm9+cCM4ekOxTnXxUXpj8BvI+1mKmrq+Pu7W7hgzhhy+mSlOxznXBcXpdG5/5U0MGG4UNKNsUbl9svji0rYW9/IBbO9BzLnXNuitDV0ppmVNw2Y2U7grNgicvvtgQWbmDIsj5ljBqQ7FOdcNxAlEWRJ6ts0IKkf0DfJ/C6NVm+rYuHGci6Y488OOOeiiZII7gH+JekKSZ8B/gncHWXlks6QtELSaknfTjLfEZIaJH08WtiuNQ8uLCKrlzj/8NHpDsU5101EqSz+saR3gNMBAd8zs3+0tVz4MNqvgQ8ARcCbkh41s6UtzPcjoM11uuQaGo2HFhZx8tShDMv3Zwecc9FEqSzOBZ4ys28AtwF9JUXp8PZIYLWZrTWzWuB+4LwW5vsS8CCwLXrYriUvrCpla8VePj7bnyR2zkUXpWjoBSBH0mjgaeBy4K4Iy40GNiUMF4Xj3hOu8yPAvCjBuuQeWFBEYf8+nHagPzvgnIsuSiKQmVUDHwV+ZWYfAWZEWa6Fcc37MfgFcG3YlEXrK5KukjRf0vzS0tIIm8485dW1/HPJVs47bDTZvaN8rM45F4jyZLEkHQNcClzRjuWKgMQb2ccAxc3mmQPcH97dMgQ4S1K9mf01cSYzu42gWIo5c+Z4pzgt+NviEmobGr1YyDnXblFO6F8F/hN42MyWSJoEPBthuTeBKZImApuBi4FLEmcws4lN7yXdBTzePAm4aP61bBvjBvXnoFHeC5lzrn2i3DX0PPB8WGmMma0FvhxhuXpJ1xDcDZQF3BEmkqvD6V4v0En21Dbw8urtzD1ynD874JxrtzYTQVgs9HsgDxgn6VDgc2b2hbaWNbMngCeajWsxAZjZZVECdvt6de129tY3csr0YekOxTnXDUWpVfwF8CFgB4CZLQJOjDEm107PLN9Gvz5ZHDVxULpDcc51Q5FuLzGzTc1GJb3Lx6WOmfHs8lKOnzLEWxp1znVIlESwSdKxgEnKlvQNYFnMcbmIVm6tYnP5Hk71YiHnXAdFSQRXA18keBisCDgsHHZdwDPLgweyT5nmicA51zFR7hraTvAMgeuCnlm+lRkjC7xfYudch7WaCCT9in2fBH6PmbV5C6mLV3l1LQs27OQLJ09OdyjOuW4sWdHQfGABkAPMAlaFr8PwyuIu4fmVpTQanHqgFws55zqu1SsCM7sbQNJlwClmVhcOzwOeSkl0Lqlnl29jUG42h44ZmO5QnHPdWJTK4lFAfsJwXjjOpVFDo/H8ylJOnjqUrF7+NLFzruOitDX0Q+AtSU3tC50E3BBbRC6StzftZGd1nT9N7Jzbb1HuGrpT0t+Bo8JR3zazLfGG5dryr2XbyOolTpw6NN2hOOe6uShXBIQn/kdijsW1wzPLtzFnfCED+kXpLM4551rnPZh0Q8Xle1i+pdKfJnbOdYpWE0HYj4Drgp5dETxN7InAOdcZkl0RPAAg6V8pisVF9OzybYwp7MfkYXnpDsU51wMkqyPoJel6YKqkrzefaGY/iy8s15qaugZeXr2DC+aM8U5onHOdItkVwcVADUGyyG/h5dLg1bU72FPX4MVCzrlOk+zJ4hXAjyS9Y2Z/T2FMLonnV5TSr08WR08anO5QnHM9RJS7hl6R9DNJ88PXTZIGxB6Za9Eb68qYPb7QO6FxznWaKIngDqASuDB8VQB3xhmUa9nuvfUs31LBrHED0x2Kc64HifJA2QFm9rGE4f+R9HZM8bgkFhWV02hw+PjCdIfinOtBolwR7JF0fNOApOOAPfGF5Frz1sZyAA4fOzCtcTjnepYoVwRXA39IqBfYCXw6vpBca97auJNJQ3MZ2D873aE453qQKI3OLQIOlVQQDlfEHpXbh5mxcGO53zbqnOt0kRqdA08A6baxrJqy3bUc7hXFzrlO5o3OdRMLN+4EYNY4ryh2znUuTwTdxMIN5eT17c3U4f5Qt3Ouc7WZCCT1l/Tfkn4XDk+RdE78oblECzfu5NCxA7xbSudcp4tyRXAnsBc4JhwuAm6MLSK3j+raepZvqeTwsV4s5JzrfFESwQFm9mOgDsDM9gD+szSF3inaRUOjMWv8wHSH4pzrgaIkglpJ/QADkHQAwRWCS5GmimK/InDOxSHK7aPXA08CYyXdCxwHXBZnUO793tpYzsQhuRTm+oNkzrnOF+WBsn9KWggcTVAk9BUz2x57ZA4IHiR7a+NOTpw6NN2hOOd6qFYTgaRZzUaVhH/HSRpnZgvjC8s12VS2h+1Vtf78gHMuNsmuCG4K/+YAc4BFBFcEM4HXgeNbWe49ks4AbgaygNvN7IfNpl8KXBsOVgGfD5u0cKG3NvmDZM65eLVaWWxmp5jZKcAGYJaZzTGz2cDhwOq2ViwpC/g1cCYwA5graUaz2dYBJ5nZTOB7wG0d+zd6roUbdtI/O4upw72jeudcPKLcNTTdzBY3DZjZu8BhEZY7ElhtZmvNrBa4HzgvcQYze8XMdoaDrwFjIkWdQRZuLOfQMQPpneUPgTvn4hHl7LJM0u2STpZ0UviE8bIIy40GNiUMF4XjWnMF0GLfyJKuauoqs7S0NMKme4Y9tQ0sK6nw5wecc7GKkgguB5YAXwG+CiwNx7WlpYfOrMUZpVMIEsG1LU03s9vCoqk5Q4dmzt0zizfvor7R/PkB51ysotw+WgP8PHy1RxEwNmF4DFDcfCZJM4HbgTPNbEc7t9GjvfcgmTc97ZyLUZuJQNI6Wvglb2aT2lj0TWCKpInAZuBi4JJm6x4HPAR80sxWRg06UyzcsJMJg/szOK9vukNxzvVgUZ4snpPwPge4ABjU1kJmVi/pGuAfBLeP3mFmSyRdHU6fB1wHDAZulQRQb2ZzWltnJjEz3tpUzgmTh6Q7FOdcDxelaKh5cc0vJL1EcBJva9kngCeajZuX8P6zwGejhZpZinbuobRyrxcLOediF6VoKPEJ414EVwjeO0rM/l0/4BXFzrl4RSkauinhfT3BQ2AXxhOOa/LWxnL69cli+gjPuc65eEVJBFeY2drEEWEFsIvRWxt3MnPMAH+QzDkXuyhnmQcijnOdpK6hkWVbKpk5ZkC6Q3HOZYBkrY9OBw4CBkj6aMKkAoK7h1xM1pbupra+kYNGeSJwzsUvWdHQNOAcYCBwbsL4SuDKGGPKeEtLdgFw0KiCNEfinMsErSYCM3sEeETSMWb2agpjynhLiyvo27sXE4fkpjsU51wGSFY09K2w0/pLJM1tPt3MvhxrZBlsaUkF00fke0Wxcy4lkhUNNbUwOj8VgbiAmbGkuIIzDx6R7lCccxkiWdHQY+Hfu1MXjivZVUN5dR0zRnr9gHMuNZIVDT1GK81GA5jZh2OJKMMtLa4AYIZXFDvnUiRZ0dBPUxaFe8/SkgokmDbCE4FzLjWSFQ093/ReUjYwneAKYUXY9aSLwdLiCiYMziWvb5SHvp1zbv9FaXTubGAesIag17GJkj5nZi12K+n2z9KSCg4Z7Q+SOedSJ8r9iTcBp5jZyWZ2EnAK7e+tzEVQUVPHxrJqrx9wzqVUlESwzcxWJwyvBbbFFE9GW15SCXhFsXMutaIURC+R9ATwZ4I6gguAN5vaHzKzh2KML6MsLQ6blvBbR51zKRQlEeQAW4GTwuFSgq4qzyVIDJ4IOsmS4gqG5GUzNN/7KHbOpU6UriovT0UgLqgoPnBkAWH/zc45lxJR7hqaCHwJmJA4vz9Q1rlq6xtZtbWKy4+fkO5QnHMZJkrR0F+B3wOPAY2xRpPB1pRWUdvQ6E1LOOdSLkoiqDGzX8YeSYZralrC+yBwzqValERws6TrgaeAvU0jzWxhbFFloKUlFeT06cXEIXnpDsU5l2GiJIJDgE8Cp/LvoiELh10nWVpcwbQRBWT18opi51xqRUkEHwEmeftC8TEzlpZUcPbMkekOxTmXgaI8WbyIoN9iF5PiXTXs2uN9EDjn0iPKFcFwYLmkN3l/HYHfPtpJlmwOnij2piWcc+kQJRFcH3sUGa6pD4LpI/LTHYpzLgNFebL4+cRhSccBlwDPt7yEa6+lxRVMHJJL/2zvg8A5l3qRzjySDiM4+V8IrAMejDGmjLO0pILDxg5MdxjOuQyVrM/iqcDFwFxgB/AnQGZ2Sopiywi79tRRtHMPlxw1Lt2hOOcyVLIrguXAi8C5Tf0RSPpaSqLKIMtKws7q/Y4h51yaJLt99GPAFuBZSb+TdBpBV5WuEzU1LeF3DDnn0qXVRGBmD5vZRQSd1j8HfA0YLuk3kj4YZeWSzpC0QtJqSd9uYbok/TKc/o6kWR38P7qtpSUVDM3vy7D8nHSH4pzLUG0+UGZmu83sXjM7BxgDvA3sc1JvTlIW8GvgTGAGMFfSjGaznQlMCV9XAb9pV/Q9wNLiCi8Wcs6lVbvuVzSzMuC34astRwKrzWwtgKT7gfOApQnznAf8wcwMeE3SQEkjzaykPXFF8fzKUm58fGnbM6bYmtIqTpp2QLrDcM5lsDhvXB8NbEoYLgKOijDPaOB9iUDSVQRXDIwb17G7a/L69mbK8K7Xsuf0kQV8bNbodIfhnMtgcSaCliqWrQPzYGa3AbcBzJkzZ5/pUcweX8js8bM7sqhzzvVoURqd66giYGzC8BiguAPzOOeci1GcieBNYIqkiZKyCR5Oe7TZPI8CnwrvHjoa2BVH/YBzzrnWxVY0ZGb1kq4B/gFkAXeY2RJJV4fT5wFPAGcBq4Fq4PK44nHOOdeyWFs5M7MnCE72iePmJbw34ItxxuCccy65OIuGnHPOdQOeCJxzLsN5InDOuQznicA55zKcgvra7kNSKbChg4sPAbZ3YjidpavGBV03No+rfTyu9umJcY03s6EtTeh2iWB/SJpvZnPSHUdzXTUu6LqxeVzt43G1T6bF5UVDzjmX4TwROOdchsu0RHBbugNoRVeNC7pubB5X+3hc7ZNRcWVUHYFzzrl9ZdoVgXPOuWY8ETjnXIbrMYlA0hmSVkhaLWmfPpXDpq5/GU5/R9KsqMvGHNelYTzvSHpF0qEJ09ZLWizpbUnzUxzXyZJ2hdt+W9J1UZeNOa5vJsT0rqQGSYPCaXHurzskbZP0bivT03V8tRVXuo6vtuJK1/HVVlwpP74kjZX0rKRlkpZI+koL88R7fJlZt38RNHO9BpgEZAOLgBnN5jkL+DtBr2hHA69HXTbmuI4FCsP3ZzbFFQ6vB4akaX+dDDzekWXjjKvZ/OcCz8S9v8J1nwjMAt5tZXrKj6+IcaX8+IoYV8qPryhxpeP4AkYCs8L3+cDKVJ+/esoVwZHAajNba2a1wP3Aec3mOQ/4gwVeAwZKGhlx2djiMrNXzGxnOPgaQS9tcduf/zmt+6uZucB9nbTtpMzsBaAsySzpOL7ajCtNx1eU/dWatO6vZlJyfJlZiZktDN9XAssI+m5PFOvx1VMSwWhgU8JwEfvuyNbmibJsnHEluoIg6zcx4ClJCyRd1UkxtSeuYyQtkvR3SQe1c9k440JSf+AM4MGE0XHtryjScXy1V6qOr6hSfXxFlq7jS9IE4HDg9WaTYj2+Yu2YJoXUwrjm98W2Nk+UZTsq8rolnULwRT0+YfRxZlYsaRjwT0nLw180qYhrIUHbJFWSzgL+CkyJuGyccTU5F3jZzBJ/3cW1v6JIx/EVWYqPryjScXy1R8qPL0l5BInnq2ZW0XxyC4t02vHVU64IioCxCcNjgOKI80RZNs64kDQTuB04z8x2NI03s+Lw7zbgYYLLwJTEZWYVZlYVvn8C6CNpSJRl44wrwcU0u2yPcX9FkY7jK5I0HF9tStPx1R4pPb4k9SFIAvea2UMtzBLv8dXZFR/peBFc2awFJvLvCpODms1zNu+vbHkj6rIxxzWOoM/mY5uNzwXyE96/ApyRwrhG8O8HDo8ENob7Lq37K5xvAEE5b24q9lfCNibQeuVnyo+viHGl/PiKGFfKj68ocaXj+Ar/7z8Av0gyT6zHV48oGjKzeknXAP8gqEW/w8yWSLo6nD6PoO/kswi+FNXA5cmWTWFc1wGDgVslAdRb0LrgcODhcFxv4I9m9mQK4/o48HlJ9cAe4GILjrx07y+AjwBPmdnuhMVj218Aku4juNNliKQi4HqgT0JcKT++IsaV8uMrYlwpP74ixgWpP76OAz4JLJb0djju/xEk8ZQcX97EhHPOZbieUkfgnHOugzwROOdchvNE4JxzGc4TgXPOZThPBM45l+E8EbgeS9JHJJmk6Z24zpMlPR6+/3BTa4+Szpc0owPre05Suzojl9Rb0nZJP2jv9pxriScC15PNBV4ieEq005nZo2b2w3DwfKDdiaCDPgisAC5UeGO7c/vDE4HrkcJ2W44jaF/n4oTxJ0t6XtKfJa2U9EMFbfa/EbY1f0A4312S5kl6MZzvnBa2cZmkWyQdC3wY+EnYVv0Bib/0JQ2RtD5830/S/WGb8n8C+iWs74OSXpW0UNJfwv+hJXOBmwmexj26E3aXy3CeCFxPdT7wpJmtBMoSO/IADgW+AhxC8ETnVDM7kqA9ni8lzDcBOIng8f55knJa2pCZvQI8CnzTzA4zszVJ4vo8UG1mM4HvA7MhSBbAd4DTzWwWMB/4evOFJfUDTgMeJ2gLZ26SbTkXiScC11PNJWibnfBv4gnzTQvagN9L0KnHU+H4xQQn/yZ/NrNGM1tF0J5LZ9Q1nAjcA2Bm7wDvhOOPJihaejlsZuDTwPgWlj8HeNbMqgkaKfuIpKxOiMtlsB7R1pBziSQNBk4FDpZkBG2wmKRvhbPsTZi9MWG4kfd/J5q3v9Ke9ljq+fcPreZXEi2tR8A/zaytX/hzgeOaipoI2hE6BXi6HbE59z5+ReB6oo8T9OY03swmmNlYYB3vb4s/igsk9QrrDSYRVNC2ppKgm8Em6wmLfcJ4mrwAXAog6WBgZjj+NYIT/ORwWn9JUxM3IKkg/B/Ghf/XBOCLePGQ20+eCFxPNJegvfhEDwKXtHM9K4DnCZr/vdrMapLMez/wTUlvhYnjpwSta74CDEmY7zdAnqR3gG8BbwCYWSlwGXBfOO019i2K+ihBH7qJVzSPAB+W1Led/5tz7/HWR51rgaS7CDpXfyDdsTgXN78icM65DOdXBM45l+H8isA55zKcJwLnnMtwngiccy7DeSJwzrkM54nAOecy3P8HuqBOU5aSpegAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEWCAYAAAB42tAoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAA32ElEQVR4nO3deXxcZdn/8c83Syddkm7pvpeWspWllLIUsAgqFhQU5KGAgCgIPij+XHFfHn3ExxVERcSFfRMQREQBC6hAgZalpQst3du0adI2SdNmv35/nJN2miaTk+XMZLner9e8MjPnzLmvzJzkmns59y0zwznnnGtJVqYDcM4517V5onDOOZeSJwrnnHMpeaJwzjmXkicK55xzKXmicM45l5Inih5C0sWS/pH02CRNibJvJ8cxTdJrkiokfSaOMlood7ykXZKyYzj2bEkrw+Of29nHb6XsOZI2xnDcP0r6Xlu3teH4l0v6d0eO0RmaxhF+hpMzEEds52c6eKLoIElrJe0J/zHulPSCpKslpfW9NbO7zey9nb1vO3wJeNbM8s3sppjKaHzfz2h8bGbrzWyAmdXHUNx3gZvD4/85huO7NAk/w9UZKHe/81PSs5I+ke442ssTRef4gJnlAxOAG4AvA79LV+GSctJVVgQTgLcyHUQna/fv1MU+G5cBPeEc8ETRicyszMweA/4LuEzSEQCSEpJ+LGm9pK2SbpHUN9xWKOnxsDayXdK/GmsjksZJeljSNkmlkm4On79c0n8k/UzSduDbLVT150paLalE0o+Sjtu0Om5hLWilpB2SfilJ4bZsST8Jj7FG0rXh/gec/JL+CZwG3BxWsw9u+s2pLWWH26+UtCyssS2VNEPSncB44C9hOV+SNDE5LkmjJT0WvqerJF2ZdMxvS3pA0h3hcd+SNLO5z1TSO8DkpLISEY79J0l3SSoHLm/mmGcpaJ4rl7RB0rebK7vJa74afgZrJV0c9ViSTg5ruTvD7c3Fky9pvqSbkt/7cNvg8PzcFn4+j0sam7T98vAcqwjPj4ubvP7H4evWSHp/it9vraQvSHpTUpmk+yXlJW2/Mnyvt4fv/eikbSnPoSbl7G2SVdDE9ktJfw3jXyDpoKR9D5H0VFjmCkkXRHnfk87Fj0taD/wz+fyU9H3gFPb9ndwcxvGTJrH+RdJnW3rP0srM/NaBG7AWOKOZ59cD14T3fw48BgwB8oG/AD8It/0AuAXIDW+nAAKygTeAnwH9gTzg5PA1lwN1wKeBHKBv+Ny/k8o3YH5Y5njgbeATSa9vuu/jwKBw323AmeG2q4GlwFhgMPB0uH9OC+/Hs43ltPC4LWV/BNgEHBe+J1OACc2978DE5LiA54Bfhe/b0eFxTw+3fRuoAuaG7/MPgJeifsYRjl0LnEvwRaxvM8ebA0wPtx8JbAXObaHsOeFn/VMgAbwLqASmtXas8P2sAOYRnFtDgaPDbX8Evhc+9zLwvaQy/9j4ONx+HtCP4Nx9EPhzuK0/UJ4Uyyjg8KTPuRa4MnyPrwE2A0rxHr8MjCY4Z5cBV4fb3g2UADPC9+AXwPMRz6HLOfB8m5L0e24HZhH8Hd0N3Jf0u20APhZumxHGcHiE931iWM4d4XH6cuD5+Sz7/13MCt+frPBxIbAbGJHp/3Fm5jWKGG0GhoTfbK4E/p+ZbTezCuB/gQvD/WoJ/sAmmFmtmf3LgjNlFsEfzRfNrNLMqswsucaw2cx+YWZ1ZranhRh+GJa5niBZzUsR7w1mtjPcdz7BP0CAC4AbzWyjme0gaFrrbC2V/Qng/8zsFQusMrN1rR1M0jjgZODL4fv2OnAb8NGk3f5tZk9Y0GZ8J3BUlEAjHvtFM/uzmTU099mY2bNmtjjc/iZwL0ECSOUbZlZtZs8BfyX4XFo71sXA02Z2b3hulYbxNhpNkPQeNLOvN1do+JqHzGx3eO5+v0msDcARkvqaWZGZJTfRrTOz34bv8e0E5/mIFL/jTWa22cy2E3yZOjrp9/i9mS0ys2rgK8CJkiYmvbalc6g1D5vZy2ZWR5AoGl93NrDWzP4Q/o0tAh4Czg/flyif4bfDv92W/j73MrOXgTLg9PCpCwn6+rZG/D1i5YkiPmMIvq0MI/g2tjCs/u8EngyfB/gRsAr4R1iFvz58fhzBH1pdC8ffECGG5H3WEfxjaMmWpPu7gQHh/dFNjhOl3LZqqexxwDvtON5ooDEpN1pH8Jm0VGaeorUlRzl2yvdI0vFhU882SWUEtbbCFC/ZYWaVTcobHeFYrb1/ZxF8270lRaz9JP1G0rqwKe15YJCk7DCm/wrLLAqbcA5Jevne99jMdod3B9CyVOfg3i8IZrYLKCX155mqnChlTgCOb/ybDf9uLwZGQuTPsK1/K7cDl4T3LyH4AtMleKKIgaTjCE7ifxNUV/cQVFkHhbeBZjYAwMwqzOzzZjYZ+ADwOUmnE5xk41P884oy7e+4pPvjCWo5bVVE0OzU3DGjqCRIlI1GtuG1G4CDWtiW6vdvrM3lJz03nqAZq6OiHLu1z+YegqbIcWY2kOAfdbNt6qHBkvo3Ka/xs0x1rFTvH8BvCb60PNHk+Mk+D0wDjjezAuDU8HkBmNnfzew9BLWF5eExO9tmgn/cQcFBrEPpnM+zJRuA55L+ZgdZMGrpmnB7lM8w1XnQ3La7gHMkHQUcCvy5Q79BJ/JE0YkkFUg6G7gPuKuxakrwx/MzScPD/cZIel94/2xJU8ImqnKgPry9TPBP+gZJ/SXlSZrdxpC+GHZGjgOuA+5vx6/1AHBdGPMgghFdbfE68OHwm+kU4ONteO1twBckHavAFEmN/zC2EnQyH8DMNgAvAD8I37cjw3LvbmPscR07n6BWUiVpFnBRhNd8R1IfSacQNIs8GOFYdwNnSLog7EQdKunoJse9FlgBPK5wgEUzse4BdkoaAnyrcYOkEZI+GP7jrgZ2EZy7ne0e4GOSjpaUIGi6XWBma2Moq9HjwMGSPiopN7wdJ+nQcHt7PsNkB5y/ZrYReIWgJvFQlCardPFE0Tn+IqmC4FvI1wg6Hj+WtP3LBM1LL4XV96cJvqUBTA0f7wJeBH4Vtn/WE9QwphB0jG8kqOa3xaPAQoJ/1n+lfUN2fwv8A3gTeA14gqBzNeo/hJ8BNQR/GLfThn+oZvYgQZv4PQSdsn8m6OiEoAP662GzwBeaefk8gg7EzcAjwLfM7KmoZbeio8f+FPDd8Jz5JkEyTmULsCMs726CTt7lrR0rbK+fS1Ar2E5wHuzXFxP2h11FcO4+qqSRRqGfEzRPlQAvEdRAGmWFx94cHv9dYTydysyeAb5B0EdQRFBLujDlizpeZgXw3rCczQSfwQ8JOtOh7Z9hUzcC5ysYpZV8vdHtBJ3kXabZCcIRCM5FpWCI4y1mNqHVnZ1zbSLpVIImqIlha0SX4DUKl5KkvpLmhk0XYwiaHh7JdFzO9TSScgmaiG/rSkkCPFG41gn4DkHTx2sE49u/mdGInOthwr6PnQSDAn6e0WCa4U1PzjnnUvIahXPOuZS6/WRVzSksLLSJEydmOgznnOs2Fi5cWGJmw5rb1iMTxcSJE3n11VczHYZzznUbklqcHsebnpxzzqXkicI551xKniicc86l5InCOedcSp4onHPOpZTRRCHpTAVLDK5KWochebsULM+4SsESiTMyEadzzvVmGUsUkrKBXwLvBw4D5kk6rMlu7yeYXXUqwQyXv05rkM455zJ6HcUsYJWZrQaQdB9wDsH6zI3OAe4Ip0J+SdIgSaPMrCiOgG56ZiV19V1qLi4Ajp88lNlTUi2A5pxz8clkohjD/ksFbgSOj7DPGII56fcj6SqCWgfjx49vV0C3PPcOe2rjWHel/czg0GXF/O26UzIdinOul8pkomhu6cemMxRG2Sd40uxW4FaAmTNntmumw6XfPbM9L4vVZ+59jcWbyjIdhnOuF8tkZ/ZG9l9/eSwHrukcZZ8eLZGTRXUXq+U453qXTCaKV4CpkiZJ6kOw5OBjTfZ5DLg0HP10AlAWV/9EV5XIzaK6ruv1mzjneo+MNT2ZWZ2ka4G/A9nA783sLUlXh9tvIVifeS7BetO72X8d6l4hkZPticI5l1EZnT3WzJ4gSAbJz92SdN+A/053XF1JIieL6jpvenLOZY5fmd3FJXKyqa036ht8JULnXGZ4oujiErnBR1TjzU/OuQzxRNHFJXKCj8ibn5xzmeKJootL5GQDeIe2cy5jPFF0cXtrFLWeKJxzmeGJootr7KPwpifnXKZ4oujivOnJOZdpnii6OO/Mds5lmieKLs77KJxzmeaJootL5HrTk3Mus1qdwkPSTOAUYDSwB1gCPG1m22OOzeFNT865zGuxRiHpckmLgK8AfYEVQDFwMvCUpNsltW+FIBdZY6Ko8qYn51yGpKpR9Admm9me5jZKOppgLev1McTlQvuanrxG4ZzLjBYThZn9sqVtko4zs1fiCckl29f05DUK51xmRJ5mXNJhBIsLzQPKgJlxBeX28VFPzrlMS5koJE0gSAzzgDpgAjDTzNbGH5qD5AvuvOnJOZcZqTqzXyBYVCgXON/MjgUqPEmkV262kLzpyTmXOamuo9gG5AMjgGHhc756TppJIs+XQ3XOZVCLicLMzgGmA4uA70haAwyWNCtdwblAIjeL6lpvenLOZUbKPgozKwN+D/xe0nDgv4CfSxpnZuPSEaBrXDfbaxTOucyIPOrJzIqBXwC/CDu5XZokvOnJORdqaDDK9tRSWlnNtooaSiurKd1VQ+muagz4/HundXqZLSYKSbcCvzCzxc1sLpF0BVBtZnd3elRuP0GNwpuenOupzIydu2vZtquabRX7biW7qtm2q5qSXTWUhI9LK2uobziwu1iCCUP6pTdRAL8CviFpOsH8TtuAPIKrsQsImqQ8SaRB0EfhNQrnupv6BqO0spri8mq2llextbya4ooqiiuC57aF90t2VVNbf+A//z7ZWRQO6ENhfoKRA/M4YkwBhQMSDB2QoHBAH4aF94cO6MPgfn3IzlIsv0eqK7NfBy6QNIDg4rpRBJMCLjOzFbFE45rlTU/OdT27quvYUlYV3Mqr2FpeRVHZniAZhElh267qZr/9D+nfh+H5CYblJzho+ACG5+cxLHw8bMC+nwV9c5Di+effFq32UZjZLuDZ+ENxLfGmJ+fSa09NPZvL9rB55x6KdlaxuWwPW8qq2FxWxZay4LmK6roDXjeoXy4j8vMYMTCPg0fkM6IgjxEFCYYX5DE8P8GIgjwKByTok9O9VnhI1Ucxn5avmzAzOz2ekFxTiZwsdjVzUjrn2s7M2LG7lo07drNpxx427dzDxh1BUgiSQxXbK2sOeF3hgASjB+UxcWh/Tpw8lFGD+jKyII+RA/MYWZDHiII8+vbJzsBvFL9UNYovNPPcCcCXCKYbd2mSyMn2Pgrn2qBsdy0bduxmw/bdbNixm4079rBhe/Bz44497GlyXdKARA5jBvVl9KA8jho7iNGD+jJmUF9GDcxj9KC+jCjI63a1gM6Uqo9iYeN9Se8CvgEkgKvN7G8dKVTSEOB+YCKwFrjAzHY02WcccAcwEmgAbjWzGztSbneVyPWmJ+eSNTQYReVVrCupZP323azbvpv1pbtZt72S9aW7Ka/avwaen5fDuMH9mFTYn1OmDmPs4L6MGdyXsYP7MnZQvy7TF9BVtTYp4PsIEkQV8H0zm99J5V4PPGNmN0i6Pnz85Sb71AGfN7NFkvKBhZKeMrOlnRRDt+EX3LneqDEZrNlWyZrSStaVVLK2dDdrS4PkUJP0N5GbLcYO7se4If04Ztxgxg/px7ghfYPnBvdjYL/cDP4m3V+qPopXCOZ4+hHwYvjcjMbtZraoA+WeA8wJ799O0Fm+X6IwsyKgKLxfIWkZMAbohYnCRz25nquiqpZ3tlXyTvEu1pRUsrpkF6u3VbK2tHK/lR0TOVlMHNqfg4b15/RDhjNhaH8mDg2Sw+hBfWMbGupS1ygqgV3A+cB5QPKnYMC7O1DuiDARYGZF4fQgLZI0ETgGWJBin6uAqwDGj+9ZK7QmcnyuJ9e9mRklu2pYWVzBquJdrCrexTvbgp9by6v37pedJcYP6cfkwv6cPKWQScP6M6kwuI3IzyPLk0FGpOqjmNORA0t6mqB/oamvtfE4A4CHgM+aWXlL+5nZrcCtADNnzuxRs9wGfRReo3Ddw47KGpZvqWBlcQVvb63g7a27WLm1gh27a/fuMyCRw0HDBzB7SiFThg9gyrABHDR8AOOH9CM3u/d2GndVked6aiszO6OlbZK2ShoV1iZG0cIoKkm5BEnibjN7OKZQu7xETjZ1DUZdfQM5/kfkuoja+gZWFe9i+ZZylhdVsHxLBcu3lO9XQ8hP5DB1xADed/hIpo7I5+ARA5g6PJ8RBQnvPO5GYksUrXgMuAy4Ifz5aNMdFJxFvyO4Evyn6Q2va2lcDrXGE4XLkPKqWpZtLmdpUTlLw58rt+6ipj6o6fbJzmLK8AHMPqiQaSPzmTYyn0NGFnhC6CEylShuAB6Q9HFgPfARAEmjgdvMbC4wG/gosFjS6+HrvmpmT2Qg3oxqTBRVtQ3065PhYFyPV7anlrc2lbE4vC3ZVMba0t17txcO6MNhowdyytRhHDoqn8NGFTCxsL83GfVgbU4UYVPRdjOrbnXnFphZKXDAld1mthmYG97/N/t3oPdaiVxfN9vFo6q2nqVF5by+fievb9jJmxt37pcUxgzqyxFjCjj/2LEcPmYgh48qYFi+1xJ6m/bUKO4EDpL0kJk1d/W262SNNQq/Ott1hJmxYfseFq7fzmthYlhWVL531tKRBXkcOXYg5x87luljB3HE6AKGDkhkOGrXFbQ5UZjZGWH/wWExxOOakchprFF4onDRVdfVs2RTGQvX7QhvOynZFTQE9O+TzZFjB/Hxkydz9LhBHD1uECMH5mU4YtdVRUoUkrKBEcn7m9lbcQXl9re3RuFNTy6F3TV1LFq3k5fXlPLSmu28vmHn3quXJwztx6lTC5kxYTDHThjMwSPy/QI1F1mriULSp4FvAVsJ5lyC4IK7I2OMyyVJ5DYmCq9RuH321NTzytrtvPBOKQvWlLJ4Yxl1DUaW4IgxA7n0hAnMnDiEYycMZli+NyG59otSo7gOmBZ2QLsM2Nv05H0UvVptfQNvbNjJf1aV8p93Snht/Q5q643cbHHU2EFcdepkZk0KEkN+ns9t5DpPlESxASiLOxDXsrxcb3rqrdaX7ua5t4t57u1tvPhOKZU19Uhw+OgCrpg9iZOmFHLcxMH065Opke6uN4hydq0GnpX0V2DvkNjefhFcOnlndu+xp6aeF1eX8PzbJTz39jbWlFQCMG5IX849ZgynTC3khMlDGeQX1Lg0ipIo1oe3PuHNpZl3ZvdsxeVVPLO8mKeXbuXfq0qormsgLzeLEycP5bITJ/CuacOZOLSfX7vgMibKmtnfAQjXhLBwDW2XRns7s72PokcwM1ZsreAfb23lmWVbeWNj0LI7ZlBf5s0az7sPGc6sSUPIy+2Zy2q67ifKqKcjCC6yGxI+LgEu9eGx6eNNT92fmfHW5nKeWFzEk0u2sDpsUjp63CC+8N6DOeOwEUwbke+1BtclRWl6uhX4XOPqdpLmAL8FToovLJfMm566JzPjzY1lPLG4iCeWFLFh+x6ys8QJk4dwxcmTeO/hIxie7xe5ua4vSqLon7wEqpk9K6l/jDG5JnwKj+5lTUklf35tE4+9sZk1JZXkZInZUwq59rQpvOewkQzp7119rnuJNOpJ0jcImp8ALgHWxBeSayonO4vsLHnTUxe2raKax9/czJ9f38wbG3YiwQmThvLJUydz5hEjfZSS69aiJIorgO8ADxPM5vo88LE4g3IHSuRkedNTF1NX38D8Fdu4/5UNzF9RTH2DcdioAr469xA+cNRoRg3sm+kQnesUUUY97QA+k4ZYXApBovAaRVewpqSSB17dwEMLN1JcUc2w/ARXnjKZ82aMYeqI/EyH51ynazFRSPq5mX1W0l8I5nbaj5l9MNbI3H4SOdneR5FBtfUNPLlkC3e9tI4Fa7aTnSVOmzaMC2aO47RDhvuiPa5HS1WjaOyT+HE6AnGpJXK96SkTiiuquHfBBu5esI7iimomDO3Hl86cxvkzxjK8wEcsud6hxURhZgvDu0eb2Y3J2yRdBzwXZ2Buf970lD5mxmsbdnLHC2v56+IiauuNOdOG8cMTJ/Kug4eR5dNzu14mSmf2ZcCNTZ67vJnnXIwSOdmeKGLW0GA8vWwrv37uHV5bv5P8RA6XnDCBS0+cyKRCHxHueq9UfRTzgIuASZIeS9qUD/iU42nmo57iU1PXwKOvb+I3z69mVfEuxg3py3fPOZzzZoylf8JnZXUu1V/BC0ARUAj8JOn5CuDNOINyB0rkZnlndierrK7j3pfX87t/r6GorIpDRxVw07xjmHvESHK8c9q5vVL1UawD1km6GNhsZlUAkvoCY4G1aYnQAUHTU/meukyH0SPsqannzpfW8utn32HH7lpOmDyEG847klOnFvpcS841I0q9+gH2n9epHngQOC6WiFyzEjlZVNV601NHVNfVc/8rG7j5n6sorqjm1IOH8dkzpjJj/OBMh+ZclxYlUeSYWU3jAzOrkeTzEaSZj3pqv9r6Bh5etJGbnlnFpp17mDVpCDdfNINZk4ZkOjTnuoUoiWKbpA+a2WMAks4BSuINyzUVjHryGkVbmBnPLCvmf59YxuqSSo4aN4gbzpvOyVO8icm5toiSKK4G7pZ0M8FcTxuAS2ONyh0guODOaxRRvb21gv95fCn/WlnCQcP689tLZ3LGocM9QTjXDlHmenoHOEHSAEBmVhF/WK6pRI6PeopiR2UNP3v6be5esJ7+fbL51gcO45ITJvgUG851QJQV7hLAecBEIKfxG5mZfbe9hUoaAtwfHnMtcEE4+WBz+2YDrwKbzOzs9pbZ3TU2PZmZfytuRl19A3e+tI6fPfU2lTX1XHz8eP7fGQcz2Nd+cK7DojQ9PQqUAQuB6k4q93rgGTO7QdL14eMvt7DvdcAyoKCTyu6W8nKzaDCoazBysz1RJHtrcxnXP7SYxZvKOGVqId84+zAO9llcnes0URLFWDM7s5PLPQeYE96/HXiWZhKFpLHAWcD3gc91cgzdSvK62d6MEqiqreemZ1bym+dXM7hfLjdfdAxnTR/lNS7nOlmURPGCpOlmtrgTyx1hZkUAZlYkaXgL+/0c+BLBtCG9WiK3cTnUegb4tBIsWF3KVx5ezOqSSj5y7Fi+dtahvoqcczGJ8h/nZOBySWsImp4EmJkdmepFkp4GRjaz6WtRApN0NlBsZgslzYmw/1XAVQDjx4+PUkS3snfd7F4+8qmiqpYf/G059yxYz7ghfbnr48dz8tTCTIflXI8WJVG8vz0HNrMzWtomaaukUWFtYhRQ3Mxus4EPSpoL5AEFku4ys0taKO9W4FaAmTNnHrDQUneX3PTUW72xYSefue81NmzfzSdOnsTn3nsw/fp47cq5uEVp7LYWbh3xGMH05YQ/Hz2gULOvmNlYM5sIXAj8s6Uk0Rvsq1H0vovuGhqMW557h/N+/QJ19cb9nzyRr599mCcJ59Ikyl/aXwkSgwi+2U8CVgCHd6DcG4AHJH0cWA98BEDSaOA2M5vbgWP3SPv6KHpXjaK4vIrPP/gG/1pZwtzpI/nBh45kYL/cTIflXK8S5YK76cmPJc0APtmRQs2sFDi9mec3AwckCTN7lmBkVK/VG5ue5i8v5gsPvkFlTR03fHg6/3XcOB/R5FwGtLnubmaLJPnMsWnWm5qe6huMH/19Bbc89w6HjMzn/otOYMrwXj/wzbmMiXJldvL1C1nADGBbbBG5Zu2tUfTwpqeKqlquu+91/rm8mIuOH883zz6MvNzsTIflXK8WpUaR/FWujqDP4qF4wnEt2dtH0YObntaX7ubjt7/C6pJK/ufcI/joCRMyHZJzjtRrZt9pZh8FdprZjWmMyTWjpzc9vfhOKZ+6eyENBndeMYuTpvi1Ec51FalqFMdKmgBcIekOglFPe5nZ9lgjc/vpyZ3Zdy9Yx7cefYuJhf257dKZTCzsn+mQnHNJUiWKW4AngckEEwImJwoLn3dpsrdG0YOWQ21oMP7nr0v5w3/Wctq0Ydw47xgK8nzoq3NdTYuJwsxuAm6S9GszuyaNMblm9LQ+irr6Br780GIeWrSRK2ZP4mtnHUp2lg99da4rinIdhSeJLqBPds9JFDV1DXz2/td4YvEWPveeg/n0u6f49RHOdWE+B0I3kZOdRU6Wun1ndlVtPdfctZD5K7bx9bMO5ROneAumc12dJ4puJJGTRVU3vo5iV3Udn7j9FRas2c7/fmg6Fx3f82b5da4n8kTRjSRys7ttjaJsdy2X/eFlFm8q42cXHM25x4zJdEjOuYiiXJldwYGzxZYRrGP9eTNbHUdg7kCJnKxueWV22Z5a5v32JVYV7+JXF8/gfYc3t0yJc66rilKj+CmwGbiHYIjshQQLEq0Afs++JU1dzBI5Wd2uM7uqtp4r73iVlcUV/PbSmcyZ1tJihs65rirKehRnmtlvzKzCzMrDBYLmmtn9wOCY43NJEjndq+mpvsH47H2v8/Ka7fz4I0d5knCum4qSKBokXSApK7xdkLStx60k15UlcrtPjcLM+NZjS3jyrS18/axDOedo75NwrruKkiguBj5KsFzp1vD+JZL6AtfGGJtrojv1Udz8z1Xc9dJ6PnnqZB8C61w3F+WCu9XAB1rY/O/ODcelkpebTWV1XabDaNV9L6/nJ0+9zYePGcOXzzwk0+E45zooyqinYcCVwMTk/c3sivjCcs1J5GSxvbJr1yieWrqVrz6ymHcdPIwfnn8kWT4th3PdXpRRT48C/wKeBrpPT2oPFHRmd91EsWRTGdfes4jpYwbyq4tnkJsdpWXTOdfVRUkU/czsy7FH4loVDI/tmrm6bHct19y9kCH9+/D7y4+jf8Kv5XSup4jyle9xSXNjj8S1KpHbNTuzzYwv/OkNinZWcfNFMxg6IJHpkJxznShKoriOIFnskVQuqUJSedyBuQN11aan2/61hqeWbuUrcw/l2Al+aY1zPU2UUU/5re3j0qMrNj29unY7Nzy5nPcfMZIrZk/MdDjOuRikWjP7EDNbLmlGc9vNbFF8YbnmNE7hYWZdYv2G0l3VXHvPa4wd3Jcfnn9kl4jJOdf5UtUoPgdcBfykmW0GvDuWiFyLErnZmEFtvdEnJ7P/lOsbjM/e/zrbd9fwyKdO8iVMnevBUi2FelX487T0heNS2btudl09fXIyO/T0F/9cyb9WlvCDD0/n8NEDMxqLcy5eUS64ywM+BZxMUJP4F3CLmVXFHJtrYl+iaCCTHUcvvFPCjc+s5MPHjOHC48ZlMBLnXDpEGex+B1AB/CJ8PA+4E/hIXEG55iVysoHMrptdVVvPVx5ezIQh/fjeh47wfgnneoEoiWKamR2V9Hi+pDc6UqikIcD9BNOCrAUuMLMdzew3CLgNOIKgNnOFmb3YkbK7s0RuWKOozdzIp5ueWcm60t3c84nj6dfHL6pzrjeI0tD9mqQTGh9IOh74TwfLvR54xsymAs+Ej5tzI/CkmR0CHAUs62C53Vpy01MmLN9Szq3Pr+a8GWM5aUphRmJwzqVfquGxiwm+xecCl0paHz6eACztYLnnsG9lvNuBZ4H9pgmRVACcClwOYGY1QE0Hy+3WMtn01NBgfOXhxeTn5fC1sw5Ne/nOucxJ1XZwdozljjCzIgAzK5LU3NJnk4FtwB8kHQUsBK4zs8rmDijpKoLhvIwfPz6eqDNsb40iA01Pdy9Yx2vrd/LTC45iSP8+aS/fOZc5qZqedpjZOoKO7OZuKUl6WtKSZm7nRIwtB5gB/NrMjgEqabmJCjO71cxmmtnMYcOGRSyie2nso6hKc41ia3kV//fkCmZPGcqHjvGV6pzrbVLVKO4hqFUsJGhySh7eYgTf+FtkZme0tE3SVkmjwtrEKILV85raCGw0swXh4z+RIlH0BnubntJco/j2Y29RU9/A98+d7qOcnOuFWqxRmNnZCv4rvMvMJpvZpKRbR9e2fAy4LLx/GcGaF03L3wJskDQtfOp0Ot430q1lojP7qaVb+duSLXzm9KlMLOyftnKdc11HylFPZmbAIzGUewPwHkkrgfeEj5E0WtITSft9Grhb0pvA0cD/xhBLt5Huzuxd1XV889ElTBuRz1Wn+rrXzvVWUQbCvyTpODN7pbMKNbNSghpC0+c3A3OTHr8OzOyscru7vddRpGkG2Ruffpst5cEaE75anXO9V5REcRrwSUnrCDqURVDZODLWyNwB9o16ir9GsbW8ittfXMeHjxnra0w418tFSRTvjz0KF0k6m55+NX8VDQ3GdadPjb0s51zXFqU9YRSw3czWhcNltwMj4w3LNSd59tg4bd65h3tf3sBHZo5l/NB+sZblnOv6oiSKXwO7kh5Xhs+5NMvKEn2ys2KvUfxy/ioM479PmxJrOc657iFKolA4+gkAM2sgWpOVi0EiJyvWPooN23fzwKsbuGDmOMYO9tqEcy5aolgt6TOScsPbdcDquANzzUvkxrtu9i/nr0LIaxPOub2iJIqrgZOATQRXSx9POKeSS79ETnZsTU/rSit5cOFG5s0ax+hBfWMpwznX/bTahGRmxcCFaYjFRZDIia+P4hf/XEV2lviU1yacc0larVFI+j9JBWGz0zOSSiRdko7g3IH65GTFMtfTmpJKHl60kUuOn8CIgrxOP75zrvuK0vT0XjMrJ5ggcCNwMPDFWKNyLUrkxtP0dNMzK+mTk8XVc3yqDufc/qIkitzw51zgXjPbHmM8rhVB01Pn1ihWFVfw6OubuPTEiQzP99qEc25/URLFXyQtJ5hz6RlJw4CqeMNyLYmjj+KmZ1aRl5vNJ33iP+dcM1pNFGZ2PXAiMNPMaoHdBEuZugxI5GR36nUUxRVVPLG4iItmjWfogESnHdc513NE6czuB/w3+67GHo3P6JoxnX0dxUMLN1HXYMw7vmcuH+uc67goTU9/AGoIrqWAoEP7e7FF5FLqzKYnM+P+V9Yza9IQDho2oFOO6ZzreaIkioPM7P+AWgAz28P+y6K6NOrMC+5eXF3K2tLdzJs1rlOO55zrmaIkihpJfQnWyUbSQUB1rFG5FiU68TqK+17eQEFeDu8/YlSnHM851zNFmdzvW8CTwDhJdwOzgcvjDMq1LOij6HiNYkdlDU8u2cK8WePIy83uhMiccz1VlCk8npK0CDiBoMnpOjMriT0y16zGpiczQ2p/C+DDr22ipr6BC2d5J7ZzLrUWE4WkGU2eKgp/jpc03swWxReWa8m+xYsa2l0TaOzEPmrcIA4dVdCZ4TnneqBUNYqfhD/zCIbDvkFQozgSWACcHG9orjmdkSgWrd/J21t3ccOHp3dmaM65HqrFzmwzO83MTgPWATPMbKaZHQscA6xKV4Buf4ncxnWz29+hfd/L6+nfJ5sPHDW6s8JyzvVgUUY9HWJmixsfmNkS4OjYInIp7a1RtPPq7IqqWh5/s4gPHj2a/glfqNA517oo/ymWSboNuItgiOwlwLJYo3ItSm56ao9HX9/Mntp6LjzOO7Gdc9FESRQfA64BrgsfP8++6TxcmiVyOtb0dN8r6zl0VAFHjh3YmWE553qwKMNjq4CfhTeXYYnc9tcolmwqY8mmcr7zwcM7NLTWOde7ROmjcF1IXmONoh19FPe9sp5EThbnHj2ms8NyzvVgGUkUkoZIekrSyvDn4Bb2+3+S3pK0RNK9knr9qjr7ahRta3qqqq3n0dc2c9b0UQzsl9v6C5xzLtRiopB0Z/jzupb26YDrgWfMbCrwTPi4afljgM8QrINxBJANXBhDLN1KezuzF6zZTkV1HR842ofEOufaJlWN4lhJE4ArJA0OawF7bx0s9xzg9vD+7cC5LeyXA/SVlAP0AzZ3sNxub19ndtsSxfzlxSRysjhx8tA4wnLO9WCpOrNvIZgMcDKwkP2nFrfw+fYaYWZFAGZWJGl40x3MbJOkHwPrgT3AP8zsHy0dUNJVwFUA48f33KGf+66jaFvT03Nvb+Okg4b6BIDOuTZLdWX2TWZ2KPB7M5tsZpOSbq0mCUlPh30LTW+RllEN+y3OASYRrKrXX9IlKeK9Nbx6fOawYcOiFNEttWfU05qSStaUVDJn2gH52DnnWhVleOw1ko4CTgmfet7M3ozwujNa2iZpq6RRYW1iFFDczG5nAGvMbFv4mocJVtm7q7Wye7L2ND09uyJ4e0/zROGca4coa2Z/BrgbGB7e7pb06Q6W+xhwWXj/MuDRZvZZD5wgqZ+CQf+n41eEJ3VmR296enbFNiYP68/4of3iCss514NFGR77CeB4M/ummX2TYF2KKztY7g3AeyStBN4TPkbSaElPAJjZAuBPwCJgcRjrrR0st9tr61xPe2rqeXF1KXMO9tqEc659okzhISD562s9HVwz28xKCWoITZ/fDMxNevwtghX2XEgSfXKir3L30upSauoaOO2Qnttv45yLV5RE8QdggaRHwsfnAr+LLSLXqkROVuSmp/kriumbm82sSR0d0eyc662idGb/VNKzBAsVCfiYmb0Wd2CuZY3LobbGzJi/opjZU4bu7QR3zrm2irQgQbjsqS992kUkcrIi9VGsLqlkw/Y9fPLUg9IQlXOup/JJAbuhRG60pqf5y4NhsXOmef+Ec679PFF0Q1Gbnp57extThw9g7GAfFuuca79IiULSBElnhPf7SsqPNyyXSiIni6pWpvCorK5jwertnHaID4t1znVMlAvuriS4nuE34VNjgT/HGJNrRSLC8NgX3imlpr6BOQd7s5NzrmOi1Cj+G5gNlAOY2UqCK7RdhiRyW296enZFMf37ZDNzog+Ldc51TJREUW1mNY0Pwim/Lb6QXGuCUU8tNz2ZGc+u2MbsKYX0yfFuKOdcx0T5L/KcpK8SrAvxHuBB4C/xhuVSSeRkUZOiRrGqeBebdu7x/gnnXKeIkiiuB7YRzLf0SeAJ4OtxBuVSa23U0/wVPizWOdd5olyZ3QD8FvhtuLLdWDPzpqcMau06ivnLt3HIyHxGDeybxqiccz1VlFFPz0oqCJPE68AfJP009shci1JdmV1RVcur67b7IkXOuU4TpelpoJmVAx8G/mBmxxIsKuQyJC/FqKdX1+6gtt449eDCNEflnOupoiSKnHAVuguAx2OOx0WQyMmipr6BhoYDWwDf2lwGwPQxA9MdlnOuh4qSKL4L/B1YZWavSJoMrIw3LJdK40ywNfUH1iqWFVUwfkg/8vNy0x2Wc66HitKZ/SDBkNjGx6uB8+IMyqWWvMpdXu7+04cvLSrnsFEFmQjLOddDtZooJOUBHwcOB/IanzezK2KMy6WQyE1eN3tfzaGyuo61pZWce/SYDEXmnOuJojQ93QmMBN4HPEcw11NFnEG51Bqbnpp2aC/fUoEZHDbaaxTOuc4TJVFMMbNvAJVmdjtwFjA93rBcKnubnppcS7GsqByAQ0f55L7Ouc4TJVHUhj93SjoCGAhMjC0i16rGRFHV5FqKpUXlFOTlMGaQX2jnnOs8UZZCvVXSYOAbwGPAAOCbsUblUkrkNt/0tKyonENHFSApE2E553qoKKOebgvvPgdMjjccF0VzTU/1DcbyogounDUuU2E553qoKKOeEgTDYScm729m340vLJfKvkSxr0axrrSSPbX1HOpDY51znSxK09OjQBmwEKiONxwXxd5RT0l9FEvDjmy/hsI519miJIqxZnZm7JG4yPa/jiKwrKicnCwxZfiATIXlnOuhoox6ekGSD4ftQpprelq6uZyDhg044Ept55zrqBYThaTFkt4ETgYWSVoh6c2k59tN0kckvSWpQdLMFPudGZa7StL1HSmzJ2nugrtlRRV+oZ1zLhapmp7OjrHcJQTTlv+mpR0kZQO/BN4DbARekfSYmS2NMa5uYW/TU7hu9vbKGraUV/mFds65WKRKFFuBq4EpBMug/s7M6jqjUDNbBrQ23n8WwYy1q8N97wPOATxRNGl6Wra3I9unFnfOdb5UfRS3AzMJksT7gZ+kJaJ9xgAbkh5vDJ9rlqSrJL0q6dVt27bFHlwm9cnev0axdLNP3eGci0+qGsVhZjYdQNLvgJfbcmBJTxNMJtjU18zs0SiHaOa5FtfqNrNbgVsBZs6c2aPX9JYULIeaVKMYUZBg6IBEhiNzzvVEqRJF4xxPmFldW6eFMLOOLpe6EUi+zHgssLmDx+wxkhPF0nDqDueci0OqRHGUpPLwvoC+4WMBZmZx/2d6BZgqaRKwCbgQuCjmMruNRG421XX1VNfVs6p4F+8+ZHimQ3LO9VAt9lGYWbaZFYS3fDPLSbrfoSQh6UOSNgInAn+V9Pfw+dGSngjLrwOuJViGdRnwgJm91ZFye5JEThbVtQ2sKt5FXYN5jcI5F5soV2Z3OjN7BHikmec3A3OTHj8BPJHG0LqNxqanxo5sv4bCOReXKFdmuy4oL2x6WlZUQV5uFhOH9s90SM65HsoTRTe1t0ZRVMYhIwvIzvI1KJxz8fBE0U0lcrKpqg1qFN4/4ZyLkyeKbiqRm8Wakt2U7an1/gnnXKw8UXRTiZwsSnYFy4Mc5ldkO+di5Imim2qcQVaCaSO9RuGci48nim6qcWLACUP6MSCRkVHOzrlewhNFN9U41bh3ZDvn4uaJoptqbHryNbKdc3HzRNFNNTY9eY3CORc3TxTd1N4ahQ+Ndc7FzHtBu6mzjhxJdhaMGpiX6VCccz2cJ4puasrwfK59t18/4ZyLnzc9OeecS8kThXPOuZQ8UTjnnEvJE4VzzrmUPFE455xLyROFc865lDxROOecS8kThXPOuZRkZpmOodNJ2gasa+fLC4GSTgyns3hcbeNxtY3H1TY9Ma4JZjasuQ09MlF0hKRXzWxmpuNoyuNqG4+rbTyutultcXnTk3POuZQ8UTjnnEvJE8WBbs10AC3wuNrG42obj6ttelVc3kfhnHMuJa9ROOecS8kThXPOuZR6TaKQdKakFZJWSbq+me2SdFO4/U1JM6K+Nua4Lg7jeVPSC5KOStq2VtJiSa9LejXNcc2RVBaW/bqkb0Z9bcxxfTEppiWS6iUNCbfF+X79XlKxpCUtbM/U+dVaXJk6v1qLK1PnV2txZer8GidpvqRlkt6SdF0z+8R3jplZj78B2cA7wGSgD/AGcFiTfeYCfwMEnAAsiPramOM6CRgc3n9/Y1zh47VAYYberznA4+15bZxxNdn/A8A/436/wmOfCswAlrSwPe3nV8S40n5+RYwr7edXlLgyeH6NAmaE9/OBt9P5P6y31ChmAavMbLWZ1QD3Aec02ecc4A4LvAQMkjQq4mtji8vMXjCzHeHDl4CxnVR2h+KK6bWdfex5wL2dVHZKZvY8sD3FLpk4v1qNK0PnV5T3qyUZfb+aSOf5VWRmi8L7FcAyYEyT3WI7x3pLohgDbEh6vJED3+SW9ony2jjjSvZxgm8MjQz4h6SFkq7qpJjaEteJkt6Q9DdJh7fxtXHGhaR+wJnAQ0lPx/V+RZGJ86ut0nV+RZXu8yuyTJ5fkiYCxwALmmyK7RzLaXOU3ZOaea7puOCW9ony2vaKfGxJpxH8IZ+c9PRsM9ssaTjwlKTl4TeidMS1iGBumF2S5gJ/BqZGfG2ccTX6APAfM0v+dhjX+xVFJs6vyNJ8fkWRifOrLTJyfkkaQJCcPmtm5U03N/OSTjnHekuNYiMwLunxWGBzxH2ivDbOuJB0JHAbcI6ZlTY+b2abw5/FwCMEVcy0xGVm5Wa2K7z/BJArqTDKa+OMK8mFNGkWiPH9iiIT51ckGTi/WpWh86st0n5+ScolSBJ3m9nDzewS3zkWR8dLV7sR1JxWA5PY15lzeJN9zmL/jqCXo7425rjGA6uAk5o83x/IT7r/AnBmGuMayb4LNmcB68P3LqPvV7jfQIJ25v7peL+SyphIy52zaT+/IsaV9vMrYlxpP7+ixJWp8yv83e8Afp5in9jOsV7R9GRmdZKuBf5OMALg92b2lqSrw+23AE8QjBpYBewGPpbqtWmM65vAUOBXkgDqLJgdcgTwSPhcDnCPmT2ZxrjOB66RVAfsAS604KzM9PsF8CHgH2ZWmfTy2N4vAEn3EozUKZS0EfgWkJsUV9rPr4hxpf38ihhX2s+viHFBBs4vYDbwUWCxpNfD575KkOhjP8d8Cg/nnHMp9ZY+Cuecc+3kicI551xKniicc86l5InCOedcSp4onHPOpeSJwvVakj4kySQd0onHnCPp8fD+Bxtn6pR0rqTD2nG8ZyXNbONrciSVSPpBW8tzrjmeKFxvNg/4N8FVtp3OzB4zsxvCh+cCbU4U7fReYAVwgcKB/c51hCcK1yuFc+bMJpjf6MKk5+dIek7SA5LelnSDgjUbXg7XGjgo3O+Pkm6R9K9wv7ObKeNySTdLOgn4IPCjcK2Cg5JrCpIKJa0N7/eVdF+4nsD9QN+k471X0ouSFkl6MPwdmjMPuJHgauYTOuHtcr2cJwrXW50LPGlmbwPbkxd5AY4CrgOmE1wNe7CZzSKYD+nTSftNBN5FMHXCLZLymivIzF4AHgO+aGZHm9k7KeK6BthtZkcC3weOhSCZAF8HzjCzGcCrwOeavlhSX+B04HGCuYjmpSjLuUg8Ubjeah7BvPyEP5P/ob5iwfz/1QQLvvwjfH4xQXJo9ICZNZjZSoK5dDqjr+NU4C4AM3sTeDN8/gSCpqv/hFM4XAZMaOb1ZwPzzWw3wQRyH5KU3QlxuV6sV8z15FwySUOBdwNHSDKC+W9M0pfCXaqTdm9IetzA/n8zTee/act8OHXs+6LWtCbS3HEEPGVmrdUQ5gGzG5uyCOZxOg14ug2xObcfr1G43uh8gpXAJpjZRDMbB6xh/7UYoviIpKyw32IyQQdySyoIlrBstJawWSmMp9HzwMUAko4Ajgyff4kgAUwJt/WTdHByAZIKwt9hfPh7TQT+G29+ch3kicL1RvMI1gtI9hBwURuPswJ4jmBq56vNrCrFvvcBX5T0WphYfkwwO+oLQGHSfr8GBkh6E/gS8DKAmW0DLgfuDbe9xIFNXR8mWMM5uUb0KPBBSYk2/m7O7eWzxzrXDpL+CDxuZn/KdCzOxc1rFM4551LyGoVzzrmUvEbhnHMuJU8UzjnnUvJE4ZxzLiVPFM4551LyROGccy6l/w/bpoNNY8LQgQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "amp_range = np.linspace(0, 2, 50)\n", + "N_a = ct.describing_function(backlash, amp_range)\n", + "\n", + "plt.figure()\n", + "plt.plot(amp_range, abs(N_a))\n", + "plt.xlabel(\"Amplitude A\")\n", + "plt.ylabel(\"Amplitude of describing function, N(A)\")\n", + "plt.title(\"Describing function for a backlash nonlinearity\")\n", + "\n", + "plt.figure()\n", + "plt.plot(amp_range, np.angle(N_a))\n", + "plt.xlabel(\"Amplitude A\")\n", + "plt.ylabel(\"Phase of describing function, N(A)\")\n", + "plt.title(\"Describing function for a backlash nonlinearity\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### User-defined, static nonlinearities\n", + "\n", + "In addition to pre-defined nonlinearies, it is possible to computing describing functions for static nonlinearities. The describing function for any suitable nonlinear function can be computed numerically using the `describing_function` function." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Define a saturation nonlinearity as a simple function\n", + "def my_saturation(x):\n", + " if abs(x) >= 1:\n", + " return math.copysign(1, x)\n", + " else:\n", + " return x\n", + "\n", + "amp_range = np.linspace(0, 2, 50)\n", + "plt.plot(amp_range, ct.describing_function(my_saturation, amp_range).real)\n", + "plt.xlabel(\"Amplitude A\")\n", + "plt.ylabel(\"Describing function, N(A)\")\n", + "plt.title(\"Describing function for a saturation nonlinearity\");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stability analysis using describing functions\n", + "Describing functions can be used to assess stability of closed loop systems consisting of a linear system and a static nonlinear using a Nyquist plot." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Limit cycle position for a third order system with saturation nonlinearity\n", + "\n", + "Consider a nonlinear feedback system consisting of a third-order linear system with transfer function $H(s)$ and a saturation nonlinearity having describing function $N(a)$. Stability can be assessed by looking for points at which \n", + "\n", + "$$ H(j\\omega) N(a) = −1$$\n", + "\n", + "The `describing_function_plot` function plots $H(j\\omega)$ and $-1/N(a)$ and prints out the the amplitudes and frequencies corresponding to intersections of these curves. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(3.343977839598768, 1.4142156916757294)]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Linear dynamics\n", + "H_simple = ct.tf([8], [1, 2, 2, 1])\n", + "omega = np.logspace(-3, 3, 500)\n", + "\n", + "# Nonlinearity\n", + "F_saturation = ct.nltools.saturation_nonlinearity(1)\n", + "amp = np.linspace(00, 5, 50)\n", + "\n", + "# Describing function plot (return value = amp, freq)\n", + "ct.describing_function_plot(H_simple, F_saturation, amp, omega)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The intersection occurs at amplitude 3.3 and frequency 1.4 rad/sec (= 0.2 Hz) and thus we predict a limit cycle with amplitude 3.3 and period of approximately 5 seconds." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Create an I/O system simulation to see what happens\n", + "io_saturation = ct.NonlinearIOSystem(\n", + " None,\n", + " lambda t, x, u, params: F_saturation(u),\n", + " inputs=1, outputs=1\n", + ")\n", + "\n", + "sys = ct.feedback(ct.tf2io(H_simple), io_saturation)\n", + "T = np.linspace(0, 30, 200)\n", + "t, y = ct.input_output_response(sys, T, 0.1, 0)\n", + "plt.plot(t, y);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Limit cycle prediction with for a time-delay system with backlash\n", + "\n", + "This example demonstrates a more complicated interaction between a (non-static) nonlinearity and a higher order transfer function, resulting in multiple intersection points." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(0.6260158833531679, 0.31026194979692245),\n", + " (0.8741930326842812, 1.215641094477062)]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Linear dynamics\n", + "H_simple = ct.tf([1], [1, 2, 2, 1])\n", + "H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4\n", + "omega = np.logspace(-3, 3, 500)\n", + "\n", + "# Nonlinearity\n", + "F_backlash = ct.nltools.backlash_nonlinearity(1)\n", + "amp = np.linspace(0.6, 5, 50)\n", + "\n", + "# Describing function plot\n", + "ct.describing_function_plot(H_multiple, F_backlash, amp, omega, mirror=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From df43e69709c8a3ee503529a5dc243bc08882d227 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 28 Jan 2021 10:23:52 -0800 Subject: [PATCH 3/8] change nltools to descfcn, update docs/ and docstrings --- control/__init__.py | 2 +- control/{nltools.py => descfcn.py} | 232 ++++++++++++------ .../{nltools_test.py => descfcn_test.py} | 10 +- doc/classes.rst | 9 + doc/control.rst | 19 +- doc/index.rst | 1 + 6 files changed, 190 insertions(+), 83 deletions(-) rename control/{nltools.py => descfcn.py} (55%) rename control/tests/{nltools_test.py => descfcn_test.py} (93%) diff --git a/control/__init__.py b/control/__init__.py index f728e1ae3..57f2d2690 100644 --- a/control/__init__.py +++ b/control/__init__.py @@ -48,6 +48,7 @@ # Note: the functions we use are specified as __all__ variables in the modules from .bdalg import * from .delay import * +from .descfcn import * from .dtime import * from .freqplot import * from .lti import * @@ -55,7 +56,6 @@ from .mateqn import * from .modelsimp import * from .nichols import * -from .nltools import * from .phaseplot import * from .pzmap import * from .rlocus import * diff --git a/control/nltools.py b/control/descfcn.py similarity index 55% rename from control/nltools.py rename to control/descfcn.py index 8f4562880..c1494ec2e 100644 --- a/control/nltools.py +++ b/control/descfcn.py @@ -1,16 +1,14 @@ -# nltools.py - nonlinear feedback analysis +# descfcn.py - describing function analysis # # RMM, 23 Jan 2021 # # This module adds functions for carrying out analysis of systems with -# static nonlinear feedback functions using the circle criterion and -# describing functions. +# static nonlinear feedback functions using describing functions. # -"""The :mod:~control.nltools` module contains function for performing closed -loop analysis of systems with static nonlinearities. It is built around the -basic structure required to apply the circle criterion and describing function -analysis. +"""The :mod:~control.descfcn` module contains function for performing +closed loop analysis of systems with static nonlinearities using +describing function analysis. """ @@ -18,78 +16,124 @@ import numpy as np import matplotlib.pyplot as plt import scipy -from numpy import where, dstack, diff, meshgrid from warnings import warn from .freqplot import nyquist_plot -__all__ = ['describing_function', 'describing_function_plot', 'sector_bounds'] +__all__ = ['describing_function', 'describing_function_plot', + 'DescribingFunctionNonlinearity'] -def sector_bounds(fcn): - raise NotImplementedError("function not currently implemented") +# Class for nonlinearities with a built-in describing function +class DescribingFunctionNonlinearity(): + """Base class for nonlinear functions with a describing function + + This class is intended to be used as a base class for nonlinear functions + that have a analytically defined describing function (accessed via the + :meth:`describing_function` method). Objects using this class should also + implement a `call` method that evaluates the nonlinearity at a given point + and an `isstatic` method that is `True` if the nonlinearity has no + internal state. + + """ + def __init__(self): + """Initailize a describing function nonlinearity""" + pass + + def __call__(self, A): + raise NotImplementedError( + "__call__() not implemented for this function (internal error)") + + def describing_function(self, A): + """Return the describing function for a nonlinearity + + This method is used to allow analytical representations of the + describing function for a nonlinearity. It turns the (complex) value + of the describing function for sinusoidal input of amplitude `A`. + + """ + raise NotImplementedError( + "describing function not implemented for this function") + + def isstatic(self): + """Return True if the function has not internal state""" + raise NotImplementedError( + "isstatic() not implemented for this function (internal error)") + + # Utility function used to compute common describing functions + def _f(self, x): + return math.copysign(1, x) if abs(x) > 1 else \ + (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi def describing_function( - fcn, amp, num_points=100, zero_check=True, try_method=True): + F, A, num_points=100, zero_check=True, try_method=True): """Numerical compute the describing function of a nonlinear function The describing function of a static nonlinear function is given by magnitude and phase of the first harmonic of the function when evaluated along a sinusoidal input :math:`a \\sin \\omega t`. This function returns - the magnitude and phase of the describing function at amplitude :math:`a`. + the magnitude and phase of the describing function at amplitude :math:`A`. Parameters ---------- - fcn : callable - The function fcn() should accept a scalar number as an argument and + F : callable + The function F() should accept a scalar number as an argument and return a scalar number. For compatibility with (static) nonlinear input/output systems, the output can also return a 1D array with a single element. - amp : float or array + If the function is an object with a method `describing_function` + then this method will be used to computing the describing function + instead of a nonlinear computation. Some common nonlinearities + use the :class:`~control.DescribingFunctionNonlinearity` class, + which provides this functionality. + + A : float or array_like The amplitude(s) at which the describing function should be calculated. zero_check : bool, optional - If `True` (default) then `amp` is zero, the function will be evaluated + If `True` (default) then `A` is zero, the function will be evaluated and checked to make sure it is zero. If not, a `TypeError` exception is raised. If zero_check is `False`, no check is made on the value of the function at zero. try_method : bool, optional - If `True` (default), check the `fcn` argument to see if it is an - object with a `describing_function` method and use this to compute the - describing function. See the :class:`NonlienarFunction` class for - more information on the `describing_function` method. + If `True` (default), check the `F` argument to see if it is an object + with a `describing_function` method and use this to compute the + describing function. More information in the `describing_function` + method for the :class:`~control.DescribingFunctionNonlinearity` class. Returns ------- df : complex or array of complex - The (complex) value of the describing fuction at the given amplitude. + The (complex) value of the describing function at the given amplitude. + If the `A` parameter is an array of amplitudes, then an array of + corresponding describing function values is returned. Raises ------ TypeError - If amp < 0 or if amp = 0 and the function fcn(0) is non-zero. + If A < 0 or if A = 0 and the function F(0) is non-zero. """ # If there is an analytical solution, trying using that first - if try_method and hasattr(fcn, 'describing_function'): + if try_method and hasattr(F, 'describing_function'): # Go through all of the amplitudes we were given df = [] - for a in np.atleast_1d(amp): - df.append(fcn.describing_function(a)) - return np.array(df).reshape(np.shape(amp)) + for a in np.atleast_1d(A): + df.append(F.describing_function(a)) + return np.array(df).reshape(np.shape(A)) # # The describing function of a nonlinear function F() can be computed by # evaluating the nonlinearity over a sinusoid. The Fourier series for a - # static noninear function evaluated on a sinusoid can be written as + # static nonlinear function evaluated on a sinusoid can be written as # - # F(a\sin\omega t) = \sum_{k=1}^\infty M_k(a) \sin(k\omega t + \phi_k(a)) + # F(A\sin\omega t) = \sum_{k=1}^\infty M_k(A) \sin(k\omega t + \phi_k(A)) # # The describing function is given by the complex number # - # N(a) = M_1(a) e^{j \phi_1(a)} / a + # N(A) = M_1(A) e^{j \phi_1(A)} / A # # To compute this, we compute F(\theta) for \theta between 0 and 2 \pi, # use the identities @@ -98,7 +142,7 @@ def describing_function( # \int_0^{2\pi} \sin^2 \theta d\theta = \pi # \int_0^{2\pi} \cos^2 \theta d\theta = \pi # - # and then integate the product against \sin\theta and \cos\theta to obtain + # and then integrate the product against \sin\theta and \cos\theta to obtain # # \int_0^{2\pi} F(a\sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi # \int_0^{2\pi} F(a\sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi @@ -112,40 +156,42 @@ def describing_function( sin_theta = np.sin(theta) cos_theta = np.cos(theta) - # Initialize any internal state by going through an initial cycle - [fcn(x) for x in np.atleast_1d(amp).min() * sin_theta] + # See if this is a static nonlinearity (assume not, just in case) + if not hasattr(F, 'isstatic') or not F.isstatic(): + # Initialize any internal state by going through an initial cycle + [F(x) for x in np.atleast_1d(A).min() * sin_theta] # Go through all of the amplitudes we were given df = [] - for a in np.atleast_1d(amp): + for a in np.atleast_1d(A): # Make sure we got a valid argument if a == 0: # Check to make sure the function has zero output with zero input - if zero_check and np.squeeze(fcn(0.)) != 0: + if zero_check and np.squeeze(F(0.)) != 0: raise ValueError("function must evaluate to zero at zero") df.append(1.) continue elif a < 0: - raise ValueError("cannot evaluate describing function for amp < 0") + raise ValueError("cannot evaluate describing function for A < 0") # Save the scaling factor for to make the formulas simpler scale = dtheta / np.pi / a # Evaluate the function (twice) along a sinusoid (for internal state) - fcn_eval = np.array([fcn(x) for x in a*sin_theta]).squeeze() + F_eval = np.array([F(x) for x in a*sin_theta]).squeeze() # Compute the prjections onto sine and cosine - df_real = (fcn_eval @ sin_theta) * scale # = M_1 \cos\phi / a - df_imag = (fcn_eval @ cos_theta) * scale # = M_1 \sin\phi / a + df_real = (F_eval @ sin_theta) * scale # = M_1 \cos\phi / a + df_imag = (F_eval @ cos_theta) * scale # = M_1 \sin\phi / a df.append(df_real + 1j * df_imag) # Return the values in the same shape as they were requested - return np.array(df).reshape(np.shape(amp)) + return np.array(df).reshape(np.shape(A)) def describing_function_plot( - H, F, a, omega=None, refine=True, label="%5.2g @ %-5.2g", **kwargs): + H, F, A, omega=None, refine=True, label="%5.2g @ %-5.2g", **kwargs): """Plot a Nyquist plot with a describing function for a nonlinear system. This function generates a Nyquist plot for a closed loop system consisting @@ -159,18 +205,23 @@ def describing_function_plot( F : static nonlinear function A static nonlinearity, either a scalar function or a single-input, single-output, static input/output system. - a : list + A : list List of amplitudes to be used for the describing function plot. omega : list, optional - List of frequences to be used for the linear system Nyquist curve. + List of frequencies to be used for the linear system Nyquist curve. + label : str, optional + Formatting string used to label intersection points on the Nyquist + plot. Defaults to "%5.2g @ %-5.2g". Set to `None` to omit labels. Returns ------- - intersection_list : 1D array of 2-tuples + intersections : 1D array of 2-tuples or None A list of all amplitudes and frequencies in which :math:`H(j\\omega) N(a) = -1`, where :math:`N(a)` is the describing function associated with `F`, or `None` if there are no such points. Each pair represents - a potential limit cycle for the closed loop system. + a potential limit cycle for the closed loop system with amplitude + given by the first value of the tuple and frequency given by the + second value. """ # Start by drawing a Nyquist curve @@ -178,7 +229,7 @@ def describing_function_plot( H_vals = H_real + 1j * H_imag # Compute the describing function - df = describing_function(F, a) + df = describing_function(F, A) N_vals = -1/df # Now add the describing function curve to the plot @@ -195,7 +246,7 @@ def describing_function_plot( # Found an intersection, compute a and omega s_amp, s_omega = intersect - a_guess = (1 - s_amp) * a[i] + s_amp * a[i+1] + a_guess = (1 - s_amp) * A[i] + s_amp * A[i+1] omega_guess = (1 - s_omega) * H_omega[j] + s_omega * H_omega[j+1] # Refine the coarse estimate to get better intersection point @@ -213,16 +264,19 @@ def _cost(x): a_final, omega_final = res.x[0], res.x[1] # Add labels to the intersection points - if label: + if isinstance(label, str): pos = H(1j * omega_final) plt.text(pos.real, pos.imag, label % (a_final, omega_final)) + elif label is not None or label is not False: + raise ValueError("label must be formatting string or None") # Save the final estimate intersections.append((a_final, omega_final)) return intersections -# Figure out whether two line segments intersection + +# Utility function to figure out whether two line segments intersection def _find_intersection(L1a, L1b, L2a, L2b): # Compute the tangents for the segments L1t = L1b - L1a @@ -244,37 +298,36 @@ def _find_intersection(L1a, L1b, L2a, L2b): return None # Debugging test - np.testing.assert_almost_equal(L1a + s1 * L1t, L2a + s2 * L2t) + # np.testing.assert_almost_equal(L1a + s1 * L1t, L2a + s2 * L2t) # Intersection is within segments; return proportional distance return (s1, s2) -# Class for nonlinear functions -class NonlinearFunction(): - def sector_bounds(self, lb, ub): - raise NotImplementedError( - "sector bounds not implemented for this function") +# Saturation nonlinearity +class saturation_nonlinearity(DescribingFunctionNonlinearity): + """Create a saturation nonlinearity for use in describing function analysis - def describing_function(self, amp): - raise NotImplementedError( - "describing function not implemented for this function") + This class creates a nonlinear function representing a saturation with + given upper and lower bounds, including the describing function for the + nonlinearity. The following call creates a nonlinear function suitable + for describing function analysis: - # Function to compute the describing function - def _f(self, x): - return math.copysign(1, x) if abs(x) > 1 else \ - (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi + F = saturation_nonlinearity(ub[, lb]) + By default, the lower bound is set to the negative of the upper bound. + Asymmetric saturation functions can be created, but note that these + functions will not have zero bias and hence care must be taken in using + the nonlinearity for analysis. -# Saturation nonlinearity -class saturation_nonlinearity(NonlinearFunction): + """ def __init__(self, ub=1, lb=None): # Process arguments if lb == None: # Only received one argument; assume symmetric around zero lb, ub = -abs(ub), abs(ub) - # Make sure the bounds are sensity + # Make sure the bounds are sensible if lb > 0 or ub < 0 or lb + ub != 0: warn("asymmetric saturation; ignoring non-zero bias term") @@ -284,6 +337,9 @@ def __init__(self, ub=1, lb=None): def __call__(self, x): return np.maximum(self.lb, np.minimum(x, self.ub)) + def isstatic(self): + return True + def describing_function(self, A): if self.lb <= A and A <= self.ub: return 1. @@ -294,12 +350,28 @@ def describing_function(self, A): # Relay with hysteresis (FBS2e, Example 10.12) -class relay_hysteresis_nonlinearity(NonlinearFunction): +class relay_hysteresis_nonlinearity(DescribingFunctionNonlinearity): + """Relay w/ hysteresis nonlinearity for use in describing function analysis + + This class creates a nonlinear function representing a a relay with + symmetric upper and lower bounds of magnitude `b` and a hysteretic region + of width `c` (using the notation from [FBS2e](https://fbsbook.org), + Example 10.12,including the describing function for the nonlinearity. The + following call creates a nonlinear function suitable for describing + function analysis: + + F = relay_hysteresis_nonlinearity(b, c) + + The output of this function is `b` if `x > c` and `-b` if `x < -c`. For + `-c <= x <= c`, the value depends on the branch of the hysteresis loop (as + illustrated in Figure 10.20 of FBS2e). + + """ def __init__(self, b, c): # Initialize the state to bottom branch self.branch = -1 # lower branch - self.b = b - self.c = c + self.b = b # relay output value + self.c = c # size of hysteresis region def __call__(self, x): if x > self.c: @@ -314,6 +386,9 @@ def __call__(self, x): y = self.b return y + def isstatic(self): + return False + def describing_function(self, a): def f(x): return math.copysign(1, x) if abs(x) > 1 else \ @@ -328,7 +403,23 @@ def f(x): # Backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) -class backlash_nonlinearity(NonlinearFunction): +class backlash_nonlinearity(DescribingFunctionNonlinearity): + """Backlash nonlinearity for use in describing function analysis + + This class creates a nonlinear function representing a backlash + nonlinearity ,including the describing function for the nonlinearity. The + following call creates a nonlinear function suitable for describing + function analysis: + + F = backlash_nonlinearity(b) + + This function maintains an internal state representing the 'center' of a + mechanism with backlash. If the new input is within `b/2` of the current + center, the output is unchanged. Otherwise, the output is given by the + input shifted by `b/2`. + + """ + def __init__(self, b): self.b = b # backlash distance self.center = 0 # current center position @@ -347,6 +438,9 @@ def __call__(self, x): y.append(self.center) return(np.array(y).reshape(x_array.shape)) + def isstatic(self): + return False + def describing_function(self, A): if A <= self.b/2: return 0 diff --git a/control/tests/nltools_test.py b/control/tests/descfcn_test.py similarity index 93% rename from control/tests/nltools_test.py rename to control/tests/descfcn_test.py index 074feae84..e9e66d464 100644 --- a/control/tests/nltools_test.py +++ b/control/tests/descfcn_test.py @@ -1,8 +1,8 @@ -"""nltools_test.py - test static nonlinear feedback functionality +"""descfcn_test.py - test describing functions and related capabilities RMM, 23 Jan 2021 -This set of unit tests covers the various operatons of the nltools module, as +This set of unit tests covers the various operatons of the descfcn module, as well as some of the support functions associated with static nonlinearities. """ @@ -87,7 +87,7 @@ def test_saturation_describing_function(satsys): df_arr = ct.describing_function(satsys, amprange) np.testing.assert_almost_equal(df_arr, df_anal, decimal=3) -from control.nltools import saturation_nonlinearity, backlash_nonlinearity, \ +from control.descfcn import saturation_nonlinearity, backlash_nonlinearity, \ relay_hysteresis_nonlinearity @@ -116,7 +116,7 @@ def test_describing_function_plot(): omega = np.logspace(-1, 2, 100) # Saturation nonlinearity - F_saturation = ct.nltools.saturation_nonlinearity(1) + F_saturation = ct.descfcn.saturation_nonlinearity(1) amp = np.linspace(1, 4, 10) # No intersection @@ -134,7 +134,7 @@ def test_describing_function_plot(): # Multiple intersections H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4 omega = np.logspace(-1, 3, 50) - F_backlash = ct.nltools.backlash_nonlinearity(1) + F_backlash = ct.descfcn.backlash_nonlinearity(1) amp = np.linspace(0.6, 5, 50) xsects = ct.describing_function_plot(H_multiple, F_backlash, amp, omega) for a, w in xsects: diff --git a/doc/classes.rst b/doc/classes.rst index b948f23aa..20645a162 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -30,3 +30,12 @@ that allow for linear, nonlinear, and interconnected elements: LinearICSystem LinearIOSystem NonlinearIOSystem + +Additional classes +================== + +.. autosummary:: + :toctree: generated/ + + DescribingFunctionNonlinearity + diff --git a/doc/control.rst b/doc/control.rst index 500f6db3c..e8a29deb9 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -42,6 +42,7 @@ Frequency domain plotting :toctree: generated/ bode_plot + describing_function_plot nyquist_plot gangof4_plot nichols_plot @@ -85,6 +86,7 @@ Control system analysis :toctree: generated/ dcgain + describing_function evalfr freqresp margin @@ -139,14 +141,15 @@ Nonlinear system support .. autosummary:: :toctree: generated/ - find_eqpt - interconnect - linearize - input_output_response - ss2io - summing_junction - tf2io - flatsys.point_to_point + describing_function + find_eqpt + interconnect + linearize + input_output_response + ss2io + summing_junction + tf2io + flatsys.point_to_point .. _utility-and-conversions: diff --git a/doc/index.rst b/doc/index.rst index 3edd7a6f6..8cfaebc00 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,6 +8,7 @@ implements basic operations for analysis and design of feedback control systems. .. rubric:: Features - Linear input/output systems in state-space and frequency domain +- Nonlinear input/output system modeling, simulation, and analysis - Block diagram algebra: serial, parallel, and feedback interconnections - Time response: initial, step, impulse - Frequency response: Bode and Nyquist plots From 776a8148ff23c35b6d224b1effa07cc28e25edf1 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 28 Jan 2021 12:23:48 -0800 Subject: [PATCH 4/8] add unit tests for exceptions/warnings + cleanup --- control/descfcn.py | 39 ++++++++++++++++++----- control/iosys.py | 2 +- control/tests/descfcn_test.py | 59 +++++++++++++++++++++++++++++------ 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/control/descfcn.py b/control/descfcn.py index c1494ec2e..0bcd44137 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -254,9 +254,15 @@ def describing_function_plot( if refine: # Refine the answer to get more accuracy def _cost(x): + # If arguments are invalid, return a "large" value + # Note: imposing bounds messed up the optimization (?) + if x[0] < 0 or x[1] < 0: + return 1 return abs(1 + H(1j * x[1]) * describing_function(F, x[0]))**2 - res = scipy.optimize.minimize(_cost, [a_guess, omega_guess]) + res = scipy.optimize.minimize( + _cost, [a_guess, omega_guess]) + # bounds=[(A[i], A[i+1]), (H_omega[j], H_omega[j+1])]) if not res.success: warn("not able to refine result; returning estimate") @@ -322,6 +328,9 @@ class saturation_nonlinearity(DescribingFunctionNonlinearity): """ def __init__(self, ub=1, lb=None): + # Create the describing function nonlinearity object + super(saturation_nonlinearity, self).__init__() + # Process arguments if lb == None: # Only received one argument; assume symmetric around zero @@ -341,6 +350,10 @@ def isstatic(self): return True def describing_function(self, A): + # Check to make sure the amplitude is positive + if A < 0: + raise ValueError("cannot evaluate describing function for A < 0") + if self.lb <= A and A <= self.ub: return 1. else: @@ -368,6 +381,9 @@ class relay_hysteresis_nonlinearity(DescribingFunctionNonlinearity): """ def __init__(self, b, c): + # Create the describing function nonlinearity object + super(relay_hysteresis_nonlinearity, self).__init__() + # Initialize the state to bottom branch self.branch = -1 # lower branch self.b = b # relay output value @@ -389,16 +405,16 @@ def __call__(self, x): def isstatic(self): return False - def describing_function(self, a): - def f(x): - return math.copysign(1, x) if abs(x) > 1 else \ - (math.asin(x) + x * math.sqrt(1 - x**2)) * 2 / math.pi + def describing_function(self, A): + # Check to make sure the amplitude is positive + if A < 0: + raise ValueError("cannot evaluate describing function for A < 0") - if a < self.c: + if A < self.c: return np.nan - df_real = 4 * self.b * math.sqrt(1 - (self.c/a)**2) / (a * math.pi) - df_imag = -4 * self.b * self.c / (math.pi * a**2) + df_real = 4 * self.b * math.sqrt(1 - (self.c/A)**2) / (A * math.pi) + df_imag = -4 * self.b * self.c / (math.pi * A**2) return df_real + 1j * df_imag @@ -421,6 +437,9 @@ class backlash_nonlinearity(DescribingFunctionNonlinearity): """ def __init__(self, b): + # Create the describing function nonlinearity object + super(backlash_nonlinearity, self).__init__() + self.b = b # backlash distance self.center = 0 # current center position @@ -442,6 +461,10 @@ def isstatic(self): return False def describing_function(self, A): + # Check to make sure the amplitude is positive + if A < 0: + raise ValueError("cannot evaluate describing function for A < 0") + if A <= self.b/2: return 0 diff --git a/control/iosys.py b/control/iosys.py index 61f820bea..e3ee3025d 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -822,7 +822,7 @@ def __call__(sys, u, squeeze=None, params=None): # If we received any parameters, update them before calling _out() if params is not None: sys._update_params(params) - + # Evaluate the function on the argument out = sys._out(0, np.array((0,)), np.asarray(u)) _, out = _process_time_response(sys, [], out, [], squeeze=squeeze) diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index e9e66d464..c9d52d472 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -12,8 +12,12 @@ import numpy as np import control as ct import math +from control.descfcn import saturation_nonlinearity, backlash_nonlinearity, \ + relay_hysteresis_nonlinearity + -class saturation(): +# Static function via a class +class saturation_class(): # Static nonlinear saturation function def __call__(self, x, lb=-1, ub=1): return np.maximum(lb, np.minimum(x, ub)) @@ -27,10 +31,15 @@ def describing_function(self, a): return 2/math.pi * (math.asin(b) + b * math.sqrt(1 - b**2)) +# Static function without a class +def saturation(x): + return np.maximum(-1, np.minimum(x, 1)) + + # Static nonlinear system implementing saturation @pytest.fixture def satsys(): - satfcn = saturation() + satfcn = saturation_class() def _satfcn(t, x, u, params): return satfcn(u) return ct.NonlinearIOSystem(None, outfcn=_satfcn, input=1, output=1) @@ -65,16 +74,16 @@ def _misofcn(t, x, u, params={}): np.testing.assert_array_equal(miso_sys([0, 0]), [0]) np.testing.assert_array_equal(miso_sys([0, 0]), [0]) np.testing.assert_array_equal(miso_sys([0, 0], squeeze=True), [0]) - + # Test saturation describing function in multiple ways def test_saturation_describing_function(satsys): - satfcn = saturation() - + satfcn = saturation_class() + # Store the analytic describing function for comparison amprange = np.linspace(0, 10, 100) df_anal = [satfcn.describing_function(a) for a in amprange] - + # Compute describing function for a static function df_fcn = [ct.describing_function(satfcn, a) for a in amprange] np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) @@ -87,8 +96,9 @@ def test_saturation_describing_function(satsys): df_arr = ct.describing_function(satsys, amprange) np.testing.assert_almost_equal(df_arr, df_anal, decimal=3) -from control.descfcn import saturation_nonlinearity, backlash_nonlinearity, \ - relay_hysteresis_nonlinearity + # Evaluate static function at a negative amplitude + with pytest.raises(ValueError, match="cannot evaluate"): + ct.describing_function(saturation, -1) @pytest.mark.parametrize("fcn, amin, amax", [ @@ -100,7 +110,7 @@ def test_describing_function(fcn, amin, amax): # Store the analytic describing function for comparison amprange = np.linspace(amin, amax, 100) df_anal = [fcn.describing_function(a) for a in amprange] - + # Compute describing function on an array of values df_arr = ct.describing_function( fcn, amprange, zero_check=False, try_method=False) @@ -110,6 +120,11 @@ def test_describing_function(fcn, amin, amax): df_meth = ct.describing_function(fcn, amprange, zero_check=False) np.testing.assert_almost_equal(df_meth, df_anal, decimal=1) + # Make sure that evaluation at negative amplitude generates an exception + with pytest.raises(ValueError, match="cannot evaluate"): + ct.describing_function(fcn, -1) + + def test_describing_function_plot(): # Simple linear system with at most 1 intersection H_simple = ct.tf([1], [1, 2, 2, 1]) @@ -141,3 +156,29 @@ def test_describing_function_plot(): np.testing.assert_almost_equal( -1/ct.describing_function(F_backlash, a), H_multiple(1j*w), decimal=5) + +def test_describing_function_exceptions(): + # Describing function with non-zero bias + with pytest.warns(UserWarning, match="asymmetric"): + saturation = ct.descfcn.saturation_nonlinearity(lb=-1, ub=2) + assert saturation(-3) == -1 + assert saturation(3) == 2 + + # Turn off the bias check + bias = ct.describing_function(saturation, 0, zero_check=False) + + # Function should evaluate to zero at zero amplitude + f = lambda x: x + 0.5 + with pytest.raises(ValueError, match="must evaluate to zero"): + bias = ct.describing_function(f, 0, zero_check=True) + + # Evaluate at a negative amplitude + with pytest.raises(ValueError, match="cannot evaluate"): + ct.describing_function(saturation, -1) + + # Describing function with bad label + H_simple = ct.tf([8], [1, 2, 2, 1]) + F_saturation = ct.descfcn.saturation_nonlinearity(1) + amp = np.linspace(1, 4, 10) + with pytest.raises(ValueError, match="formatting string"): + ct.describing_function_plot(H_simple, F_saturation, amp, label=1) From 2ac5d9df128d924c448101b09f0d37768ca5ad5c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 28 Jan 2021 22:31:07 -0800 Subject: [PATCH 5/8] make _isstatic internal; updated doc/ for descfcn --- control/descfcn.py | 29 ++++++---- doc/classes.rst | 9 --- doc/descfcn.rst | 86 +++++++++++++++++++++++++++++ doc/describing_functions.ipynb | 1 + doc/examples.rst | 1 + doc/index.rst | 1 + examples/describing_functions.ipynb | 23 ++++---- examples/steering.ipynb | 13 +---- 8 files changed, 122 insertions(+), 41 deletions(-) create mode 100644 doc/descfcn.rst create mode 120000 doc/describing_functions.ipynb diff --git a/control/descfcn.py b/control/descfcn.py index 0bcd44137..3d2b50dd0 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -21,7 +21,8 @@ from .freqplot import nyquist_plot __all__ = ['describing_function', 'describing_function_plot', - 'DescribingFunctionNonlinearity'] + 'DescribingFunctionNonlinearity', 'backlash_nonlinearity', + 'relay_hysteresis_nonlinearity', 'saturation_nonlinearity'] # Class for nonlinearities with a built-in describing function class DescribingFunctionNonlinearity(): @@ -31,7 +32,7 @@ class DescribingFunctionNonlinearity(): that have a analytically defined describing function (accessed via the :meth:`describing_function` method). Objects using this class should also implement a `call` method that evaluates the nonlinearity at a given point - and an `isstatic` method that is `True` if the nonlinearity has no + and an `_isstatic` method that is `True` if the nonlinearity has no internal state. """ @@ -54,10 +55,10 @@ def describing_function(self, A): raise NotImplementedError( "describing function not implemented for this function") - def isstatic(self): + def _isstatic(self): """Return True if the function has not internal state""" raise NotImplementedError( - "isstatic() not implemented for this function (internal error)") + "_isstatic() not implemented for this function (internal error)") # Utility function used to compute common describing functions def _f(self, x): @@ -157,7 +158,7 @@ def describing_function( cos_theta = np.cos(theta) # See if this is a static nonlinearity (assume not, just in case) - if not hasattr(F, 'isstatic') or not F.isstatic(): + if not hasattr(F, '_isstatic') or not F._isstatic(): # Initialize any internal state by going through an initial cycle [F(x) for x in np.atleast_1d(A).min() * sin_theta] @@ -223,6 +224,14 @@ def describing_function_plot( given by the first value of the tuple and frequency given by the second value. + Example + ------- + >>> H_simple = ct.tf([8], [1, 2, 2, 1]) + >>> F_saturation = ct.descfcn.saturation_nonlinearity(1) + >>> amp = np.linspace(1, 4, 10) + >>> ct.describing_function_plot(H_simple, F_saturation, amp) + [(3.344008947853124, 1.414213099755523)] + """ # Start by drawing a Nyquist curve H_real, H_imag, H_omega = nyquist_plot(H, omega, plot=True, **kwargs) @@ -346,7 +355,7 @@ def __init__(self, ub=1, lb=None): def __call__(self, x): return np.maximum(self.lb, np.minimum(x, self.ub)) - def isstatic(self): + def _isstatic(self): return True def describing_function(self, A): @@ -369,8 +378,8 @@ class relay_hysteresis_nonlinearity(DescribingFunctionNonlinearity): This class creates a nonlinear function representing a a relay with symmetric upper and lower bounds of magnitude `b` and a hysteretic region of width `c` (using the notation from [FBS2e](https://fbsbook.org), - Example 10.12,including the describing function for the nonlinearity. The - following call creates a nonlinear function suitable for describing + Example 10.12, including the describing function for the nonlinearity. + The following call creates a nonlinear function suitable for describing function analysis: F = relay_hysteresis_nonlinearity(b, c) @@ -402,7 +411,7 @@ def __call__(self, x): y = self.b return y - def isstatic(self): + def _isstatic(self): return False def describing_function(self, A): @@ -457,7 +466,7 @@ def __call__(self, x): y.append(self.center) return(np.array(y).reshape(x_array.shape)) - def isstatic(self): + def _isstatic(self): return False def describing_function(self, A): diff --git a/doc/classes.rst b/doc/classes.rst index 20645a162..b948f23aa 100644 --- a/doc/classes.rst +++ b/doc/classes.rst @@ -30,12 +30,3 @@ that allow for linear, nonlinear, and interconnected elements: LinearICSystem LinearIOSystem NonlinearIOSystem - -Additional classes -================== - -.. autosummary:: - :toctree: generated/ - - DescribingFunctionNonlinearity - diff --git a/doc/descfcn.rst b/doc/descfcn.rst new file mode 100644 index 000000000..21a06d03f --- /dev/null +++ b/doc/descfcn.rst @@ -0,0 +1,86 @@ +.. _descfcn-module: + +******************** +Describing functions +******************** + +For nonlinear systems consisting of a feedback connection between a +linear system and a static nonlinearity, it is possible to obtain a +generalization of Nyquist's stability criterion based on the idea of +describing functions. The basic concept involves approximating the +response of a static nonlinearity to an input :math:`u = A e^{\omega +t}` as an output :math:`y = N(A) (A e^{\omega t})`, where :math:`N(A) +\in \mathbb{C}` represents the (amplitude-dependent) gain and phase +associated with the nonlinearity. + +Stability analysis of a linear system :math:`H(s)` with a feedback +nonlinearity :math:`F(x)` is done by looking for amplitudes :math:`A` +and frequencies :math:`\omega` such that + +.. math:: + + H(j\omega) N(A) = -1 + +If such an intersection exists, it indicates that there may be a limit +cycle of amplitude :math:`A` with frequency :math:`\omega`. + +Describing function analysis is a simple method, but it is approximate +because it assumes that higher harmonics can be neglected. + +Module usage +============ + +The function :func:`~control.describing_function` can be used to +compute the describing function of a nonlinear function:: + + N = ct.describing_function(F, A) + +Stability analysis using describing functions is done by looking for +amplitudes :math:`a` and frequencies :math`\omega` such that + +.. math:: + + H(j\omega) = \frac{-1}{N(A)} + +These points can be determined by generating a Nyquist plot in which the +transfer function :math:`H(j\omega)` intersections the negative +reciprocal of the describing function :math:`N(A)`. The +:func:`~control.describing_function_plot` function generates this plot +and returns the amplitude and frequency of any points of intersection:: + + ct.describing_function_plot(H, F, amp_range[, omega_range]) + + +Pre-defined nonlinearities +========================== + +To facilitate the use of common describing functions, the following +nonlinearity constructors are predefined: + +.. code:: python + + backlash_nonlinearity(b) # backlash nonlinearity with width b + relay_hysteresis_nonlinearity(b, c) # relay output of amplitude b with + # hysteresis of half-width c + saturation_nonlinearity(ub[, lb]) # saturation nonlinearity with upper + # bound and (optional) lower bound + +Calling these functions will create an object `F` that can be used for +describing function analysis. For example, to create a saturation +nonlinearity:: + + F = ct.saturation_nonlinearity(1) + +These functions use the +:class:`~control.DescribingFunctionNonlinearity`, which allows an +analytical description of the describing function. + +Module classes and functions +============================ +.. autosummary:: + :toctree: generated/ + + ~control.DescribingFunctionNonlinearity + ~control.backlash_nonlinearity + ~control.relay_hysteresis_nonlinearity + ~control.saturation_nonlinearity diff --git a/doc/describing_functions.ipynb b/doc/describing_functions.ipynb new file mode 120000 index 000000000..14bcb69a4 --- /dev/null +++ b/doc/describing_functions.ipynb @@ -0,0 +1 @@ +../examples/describing_functions.ipynb \ No newline at end of file diff --git a/doc/examples.rst b/doc/examples.rst index b1ffdfce5..e56d46e70 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -42,5 +42,6 @@ using running examples in FBS2e. :maxdepth: 1 cruise + describing_functions steering pvtol-lqr-nested diff --git a/doc/index.rst b/doc/index.rst index 8cfaebc00..3558b0b30 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -29,6 +29,7 @@ implements basic operations for analysis and design of feedback control systems. matlab flatsys iosys + descfcn examples * :ref:`genindex` diff --git a/examples/describing_functions.ipynb b/examples/describing_functions.ipynb index d46ecba95..0881ce467 100644 --- a/examples/describing_functions.ipynb +++ b/examples/describing_functions.ipynb @@ -4,9 +4,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Describing Function Analysis Using the Python Control Toolbox (python-control)\n", - "### Richard M. Murray, 27 Jan 2021\n", - "This Jupyter notebook shows how to use the `nltools` module of the Python Control Toolbox to perform describing function analysis of a nonlinear system. A brief introduction to describing functions can be found in [Feedback Systems](https://fbsbook.org), Section 10.5 (Generalized Notions of Gain and Phase)." + "# Describing function analysis\n", + "Richard M. Murray, 27 Jan 2021\n", + "\n", + "This Jupyter notebook shows how to use the `descfcn` module of the Python Control Toolbox to perform describing function analysis of a nonlinear system. A brief introduction to describing functions can be found in [Feedback Systems](https://fbsbook.org), Section 10.5 (Generalized Notions of Gain and Phase)." ] }, { @@ -35,7 +36,7 @@ "source": [ "### Saturation nonlinearity\n", "\n", - "A saturation nonlinearity can be obtained using the `ct.nltools.saturation_nonlinearity` function. This function takes the saturation level as an argument." + "A saturation nonlinearity can be obtained using the `ct.saturation_nonlinearity` function. This function takes the saturation level as an argument." ] }, { @@ -57,7 +58,7 @@ } ], "source": [ - "saturation=ct.nltools.saturation_nonlinearity(0.75)\n", + "saturation=ct.saturation_nonlinearity(0.75)\n", "x = np.linspace(-2, 2, 50)\n", "plt.plot(x, saturation(x))\n", "plt.xlabel(\"Input, x\")\n", @@ -96,7 +97,7 @@ "metadata": {}, "source": [ "### Backlash nonlinearity\n", - "A backlash nonlinearity can be obtained using the `ct.nltools.backlash_nonlinearity` function. This function takes as is argument the size of the backlash region." + "A backlash nonlinearity can be obtained using the `ct.backlash_nonlinearity` function. This function takes as is argument the size of the backlash region." ] }, { @@ -118,7 +119,7 @@ } ], "source": [ - "backlash = ct.nltools.backlash_nonlinearity(0.5)\n", + "backlash = ct.backlash_nonlinearity(0.5)\n", "theta = np.linspace(0, 2*np.pi, 50)\n", "x = np.sin(theta)\n", "plt.plot(x, backlash(x))\n", @@ -232,7 +233,9 @@ "\n", "Consider a nonlinear feedback system consisting of a third-order linear system with transfer function $H(s)$ and a saturation nonlinearity having describing function $N(a)$. Stability can be assessed by looking for points at which \n", "\n", - "$$ H(j\\omega) N(a) = −1$$\n", + "$$\n", + "H(j\\omega) N(a) = -1", + "$$\n", "\n", "The `describing_function_plot` function plots $H(j\\omega)$ and $-1/N(a)$ and prints out the the amplitudes and frequencies corresponding to intersections of these curves. " ] @@ -271,7 +274,7 @@ "omega = np.logspace(-3, 3, 500)\n", "\n", "# Nonlinearity\n", - "F_saturation = ct.nltools.saturation_nonlinearity(1)\n", + "F_saturation = ct.saturation_nonlinearity(1)\n", "amp = np.linspace(00, 5, 50)\n", "\n", "# Describing function plot (return value = amp, freq)\n", @@ -362,7 +365,7 @@ "omega = np.logspace(-3, 3, 500)\n", "\n", "# Nonlinearity\n", - "F_backlash = ct.nltools.backlash_nonlinearity(1)\n", + "F_backlash = ct.backlash_nonlinearity(1)\n", "amp = np.linspace(0.6, 5, 50)\n", "\n", "# Describing function plot\n", diff --git a/examples/steering.ipynb b/examples/steering.ipynb index 1e6b022a1..eb22a5909 100644 --- a/examples/steering.ipynb +++ b/examples/steering.ipynb @@ -5,23 +5,12 @@ "metadata": {}, "source": [ "# Vehicle steering\n", - "Karl J. Astrom and Richard M. Murray \n", + "Karl J. Astrom and Richard M. Murray\n", "23 Jul 2019\n", "\n", "This notebook contains the computations for the vehicle steering running example in *Feedback Systems*." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "RMM comments to Karl, 27 Jun 2019\n", - "* I'm using this notebook to walk through all of the vehicle steering examples and make sure that all of the parameters, conditions, and maximum steering angles are consitent and reasonable.\n", - "* Please feel free to send me comments on the contents as well as the bulletted notes, in whatever form is most convenient.\n", - "* Once we have sorted out all of the settings we want to use, I'll copy over the changes into the MATLAB files that we use for creating the figures in the book.\n", - "* These notes will be removed from the notebook once we have finalized everything." - ] - }, { "cell_type": "code", "execution_count": 1, From 17ed781721b359b0199dc18e4f39d5725a0ecbd4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 30 Jan 2021 17:27:06 -0800 Subject: [PATCH 6/8] address @roryyorke review comments --- control/descfcn.py | 90 +++++++++++++++-------------- control/freqplot.py | 6 +- control/iosys.py | 31 ++++++++-- control/tests/descfcn_test.py | 26 ++++++--- doc/descfcn.rst | 4 +- examples/describing_functions.ipynb | 2 +- 6 files changed, 96 insertions(+), 63 deletions(-) diff --git a/control/descfcn.py b/control/descfcn.py index 3d2b50dd0..aa2cc4264 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -3,11 +3,11 @@ # RMM, 23 Jan 2021 # # This module adds functions for carrying out analysis of systems with -# static nonlinear feedback functions using describing functions. +# memoryless nonlinear feedback functions using describing functions. # """The :mod:~control.descfcn` module contains function for performing -closed loop analysis of systems with static nonlinearities using +closed loop analysis of systems with memoryless nonlinearities using describing function analysis. """ @@ -26,21 +26,21 @@ # Class for nonlinearities with a built-in describing function class DescribingFunctionNonlinearity(): - """Base class for nonlinear functions with a describing function + """Base class for nonlinear systems with a describing function This class is intended to be used as a base class for nonlinear functions - that have a analytically defined describing function (accessed via the - :meth:`describing_function` method). Objects using this class should also - implement a `call` method that evaluates the nonlinearity at a given point - and an `_isstatic` method that is `True` if the nonlinearity has no - internal state. + that have an analytically defined describing function. Subclasses should + override the `__call__` and `describing_function` methods and (optionally) + the `_isstatic` method (should be `False` if `__call__` updates the + instance state). """ def __init__(self): - """Initailize a describing function nonlinearity""" + """Initailize a describing function nonlinearity (optional)""" pass def __call__(self, A): + """Evaluate the nonlinearity at a (scalar) input value""" raise NotImplementedError( "__call__() not implemented for this function (internal error)") @@ -56,9 +56,15 @@ def describing_function(self, A): "describing function not implemented for this function") def _isstatic(self): - """Return True if the function has not internal state""" - raise NotImplementedError( - "_isstatic() not implemented for this function (internal error)") + """Return True if the function has no internal state (memoryless) + + This internal function is used to optimize numerical computation of + the describing function. It can be set to `True` if the instance + maintains no internal memory of the instance state. Assumed False by + default. + + """ + return False # Utility function used to compute common describing functions def _f(self, x): @@ -70,10 +76,10 @@ def describing_function( F, A, num_points=100, zero_check=True, try_method=True): """Numerical compute the describing function of a nonlinear function - The describing function of a static nonlinear function is given by - magnitude and phase of the first harmonic of the function when evaluated - along a sinusoidal input :math:`a \\sin \\omega t`. This function returns - the magnitude and phase of the describing function at amplitude :math:`A`. + The describing function of a nonlinearity is given by magnitude and phase + of the first harmonic of the function when evaluated along a sinusoidal + input :math:`A \\sin \\omega t`. This function returns the magnitude and + phase of the describing function at amplitude :math:`A`. Parameters ---------- @@ -119,11 +125,15 @@ def describing_function( """ # If there is an analytical solution, trying using that first if try_method and hasattr(F, 'describing_function'): - # Go through all of the amplitudes we were given - df = [] - for a in np.atleast_1d(A): - df.append(F.describing_function(a)) - return np.array(df).reshape(np.shape(A)) + try: + # Go through all of the amplitudes we were given + df = [] + for a in np.atleast_1d(A): + df.append(F.describing_function(a)) + return np.array(df).reshape(np.shape(A)) + except NotImplementedError: + # Drop through and do the numerical computation + pass # # The describing function of a nonlinear function F() can be computed by @@ -136,8 +146,8 @@ def describing_function( # # N(A) = M_1(A) e^{j \phi_1(A)} / A # - # To compute this, we compute F(\theta) for \theta between 0 and 2 \pi, - # use the identities + # To compute this, we compute F(A \sin\theta) for \theta between 0 and 2 + # \pi, use the identities # # \sin(\theta + \phi) = \sin\theta \cos\phi + \cos\theta \sin\phi # \int_0^{2\pi} \sin^2 \theta d\theta = \pi @@ -145,15 +155,15 @@ def describing_function( # # and then integrate the product against \sin\theta and \cos\theta to obtain # - # \int_0^{2\pi} F(a\sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi - # \int_0^{2\pi} F(a\sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi + # \int_0^{2\pi} F(A\sin\theta) \sin\theta d\theta = M_1 \pi \cos\phi + # \int_0^{2\pi} F(A\sin\theta) \cos\theta d\theta = M_1 \pi \sin\phi # # From these we can compute M1 and \phi. # - # Evaluate over a full range of angles - theta = np.linspace(0, 2*np.pi, num_points) - dtheta = theta[1] - theta[0] + # Evaluate over a full range of angles (leave off endpoint a la DFT) + theta, dtheta = np.linspace( + 0, 2*np.pi, num_points, endpoint=False, retstep=True) sin_theta = np.sin(theta) cos_theta = np.cos(theta) @@ -175,10 +185,10 @@ def describing_function( elif a < 0: raise ValueError("cannot evaluate describing function for A < 0") - # Save the scaling factor for to make the formulas simpler + # Save the scaling factor to make the formulas simpler scale = dtheta / np.pi / a - # Evaluate the function (twice) along a sinusoid (for internal state) + # Evaluate the function along a sinusoid F_eval = np.array([F(x) for x in a*sin_theta]).squeeze() # Compute the prjections onto sine and cosine @@ -353,7 +363,7 @@ def __init__(self, ub=1, lb=None): self.ub = ub def __call__(self, x): - return np.maximum(self.lb, np.minimum(x, self.ub)) + return np.clip(x, self.lb, self.ub) def _isstatic(self): return True @@ -453,18 +463,12 @@ def __init__(self, b): self.center = 0 # current center position def __call__(self, x): - # Convert input to an array - x_array = np.array(x) - - y = [] - for x in np.atleast_1d(x_array): - # If we are outside the backlash, move and shift the center - if x - self.center > self.b/2: - self.center = x - self.b/2 - elif x - self.center < -self.b/2: - self.center = x + self.b/2 - y.append(self.center) - return(np.array(y).reshape(x_array.shape)) + # If we are outside the backlash, move and shift the center + if x - self.center > self.b/2: + self.center = x - self.b/2 + elif x - self.center < -self.b/2: + self.center = x + self.b/2 + return self.center def _isstatic(self): return False diff --git a/control/freqplot.py b/control/freqplot.py index daf73d931..03a7dadfb 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -522,10 +522,8 @@ def gen_zero_centered_series(val_min, val_max, period): def nyquist_plot( syslist, omega=None, plot=True, omega_limits=None, omega_num=None, - label_freq=0, arrowhead_length=0.1, arrowhead_width=0.1, - mirror='--', color=None, *args, **kwargs): - """ - Nyquist plot for a system + label_freq=0, color=None, mirror='--', arrowhead_length=0.1, + arrowhead_width=0.1, *args, **kwargs): """Nyquist plot for a system Plots a Nyquist plot for the system over a (optional) frequency range. diff --git a/control/iosys.py b/control/iosys.py index e3ee3025d..b1bfe9330 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -325,6 +325,10 @@ def __neg__(sys): # Return the newly created system return newsys + def _isstatic(self): + """Check to see if a system is a static system (no states)""" + return self.nstates == 0 + # Utility function to parse a list of signals def _process_signal_list(self, signals, prefix='s'): if signals is None: @@ -450,10 +454,6 @@ def issiso(self): """Check to see if a system is single input, single output""" return self.ninputs == 1 and self.noutputs == 1 - def isstatic(self): - """Check to see if a system is a static system (no states)""" - return self.nstates == 0 - def feedback(self, other=1, sign=-1, params={}): """Feedback interconnection between two input/output systems @@ -812,9 +812,28 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None, self._current_params = params.copy() # Return the value of a static nonlinear system - def __call__(sys, u, squeeze=None, params=None): + def __call__(sys, u, params=None, squeeze=None): + """Evaluate a (static) nonlinearity at a given input value + + If a nonlinear I/O system has not internal state, then evaluating the + system at an input `u` gives the output `y = F(u)`, determined by the + output function. + + Parameters + ---------- + params : dict, optional + Parameter values for the system. Passed to the evaluation function + for the system as default values, overriding internal defaults. + squeeze : bool, optional + If True and if the system has a single output, return the system + output as a 1D array rather than a 2D array. If False, return the + system output as a 2D array even if the system is SISO. Default + value set by config.defaults['control.squeeze_time_response']. + + """ + # Make sure the call makes sense - if not sys.isstatic(): + if not sys._isstatic(): raise TypeError( "function evaluation is only supported for static " "input/output systems") diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index c9d52d472..184227cb9 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -17,10 +17,10 @@ # Static function via a class -class saturation_class(): +class saturation_class: # Static nonlinear saturation function def __call__(self, x, lb=-1, ub=1): - return np.maximum(lb, np.minimum(x, ub)) + return np.clip(x, lb, ub) # Describing function for a saturation function def describing_function(self, a): @@ -33,7 +33,7 @@ def describing_function(self, a): # Static function without a class def saturation(x): - return np.maximum(-1, np.minimum(x, 1)) + return np.clip(x, -1, 1) # Static nonlinear system implementing saturation @@ -47,7 +47,7 @@ def _satfcn(t, x, u, params): def test_static_nonlinear_call(satsys): # Make sure that the saturation system is a static nonlinearity - assert satsys.isstatic() + assert satsys._isstatic() # Make sure the saturation function is doing the right computation input = [-2, -1, -0.5, 0, 0.5, 1, 2] @@ -61,7 +61,7 @@ def test_static_nonlinear_call(satsys): np.testing.assert_array_equal(satsys([0.]), [0.]) # Test SIMO nonlinearity - def _simofcn(t, x, u, params={}): + def _simofcn(t, x, u, params): return np.array([np.cos(u), np.sin(u)]) simo_sys = ct.NonlinearIOSystem(None, outfcn=_simofcn, input=1, output=2) np.testing.assert_array_equal(simo_sys([0.]), [1, 0]) @@ -72,7 +72,6 @@ def _misofcn(t, x, u, params={}): return np.array([np.sin(u[0]) * np.cos(u[1])]) miso_sys = ct.NonlinearIOSystem(None, outfcn=_misofcn, input=2, output=1) np.testing.assert_array_equal(miso_sys([0, 0]), [0]) - np.testing.assert_array_equal(miso_sys([0, 0]), [0]) np.testing.assert_array_equal(miso_sys([0, 0], squeeze=True), [0]) @@ -85,6 +84,10 @@ def test_saturation_describing_function(satsys): df_anal = [satfcn.describing_function(a) for a in amprange] # Compute describing function for a static function + df_fcn = [ct.describing_function(saturation, a) for a in amprange] + np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) + + # Compute describing function for a describing function nonlinearity df_fcn = [ct.describing_function(satfcn, a) for a in amprange] np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) @@ -100,6 +103,15 @@ def test_saturation_describing_function(satsys): with pytest.raises(ValueError, match="cannot evaluate"): ct.describing_function(saturation, -1) + # Create describing function nonlinearity w/out describing_function method + # and make sure it drops through to the underlying computation + class my_saturation(ct.DescribingFunctionNonlinearity): + def __call__(self, x): + return saturation(x) + satfcn_nometh = my_saturation() + df_nometh = [ct.describing_function(satfcn_nometh, a) for a in amprange] + np.testing.assert_almost_equal(df_nometh, df_anal, decimal=3) + @pytest.mark.parametrize("fcn, amin, amax", [ [saturation_nonlinearity(1), 0, 10], @@ -118,7 +130,7 @@ def test_describing_function(fcn, amin, amax): # Make sure the describing function method also works df_meth = ct.describing_function(fcn, amprange, zero_check=False) - np.testing.assert_almost_equal(df_meth, df_anal, decimal=1) + np.testing.assert_almost_equal(df_meth, df_anal) # Make sure that evaluation at negative amplitude generates an exception with pytest.raises(ValueError, match="cannot evaluate"): diff --git a/doc/descfcn.rst b/doc/descfcn.rst index 21a06d03f..240bbb894 100644 --- a/doc/descfcn.rst +++ b/doc/descfcn.rst @@ -8,8 +8,8 @@ For nonlinear systems consisting of a feedback connection between a linear system and a static nonlinearity, it is possible to obtain a generalization of Nyquist's stability criterion based on the idea of describing functions. The basic concept involves approximating the -response of a static nonlinearity to an input :math:`u = A e^{\omega -t}` as an output :math:`y = N(A) (A e^{\omega t})`, where :math:`N(A) +response of a static nonlinearity to an input :math:`u = A e^{j \omega +t}` as an output :math:`y = N(A) (A e^{j \omega t})`, where :math:`N(A) \in \mathbb{C}` represents the (amplitude-dependent) gain and phase associated with the nonlinearity. diff --git a/examples/describing_functions.ipynb b/examples/describing_functions.ipynb index 0881ce467..5bdf888dd 100644 --- a/examples/describing_functions.ipynb +++ b/examples/describing_functions.ipynb @@ -122,7 +122,7 @@ "backlash = ct.backlash_nonlinearity(0.5)\n", "theta = np.linspace(0, 2*np.pi, 50)\n", "x = np.sin(theta)\n", - "plt.plot(x, backlash(x))\n", + "plt.plot(x, [backlash(z) for z in x])\n", "plt.xlabel(\"Input, x\")\n", "plt.ylabel(\"Output, y = backlash(x)\")\n", "plt.title(\"Input/output map for a backlash nonlinearity\");" From b29cedf1e02c71e48fbc46221e0ecddd1dc5f634 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 31 Jan 2021 10:42:02 -0800 Subject: [PATCH 7/8] additional updates based on @roryyorke review feedback --- control/descfcn.py | 49 ++++++++++++++--------------- control/tests/descfcn_test.py | 16 +++++----- examples/describing_functions.ipynb | 8 ++--- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/control/descfcn.py b/control/descfcn.py index aa2cc4264..236125b2e 100644 --- a/control/descfcn.py +++ b/control/descfcn.py @@ -21,7 +21,7 @@ from .freqplot import nyquist_plot __all__ = ['describing_function', 'describing_function_plot', - 'DescribingFunctionNonlinearity', 'backlash_nonlinearity', + 'DescribingFunctionNonlinearity', 'friction_backlash_nonlinearity', 'relay_hysteresis_nonlinearity', 'saturation_nonlinearity'] # Class for nonlinearities with a built-in describing function @@ -95,7 +95,7 @@ def describing_function( use the :class:`~control.DescribingFunctionNonlinearity` class, which provides this functionality. - A : float or array_like + A : array_like The amplitude(s) at which the describing function should be calculated. zero_check : bool, optional @@ -112,25 +112,19 @@ def describing_function( Returns ------- - df : complex or array of complex - The (complex) value of the describing function at the given amplitude. - If the `A` parameter is an array of amplitudes, then an array of - corresponding describing function values is returned. + df : array of complex + The (complex) value of the describing function at the given amplitudes. Raises ------ TypeError - If A < 0 or if A = 0 and the function F(0) is non-zero. + If A[i] < 0 or if A[i] = 0 and the function F(0) is non-zero. """ # If there is an analytical solution, trying using that first if try_method and hasattr(F, 'describing_function'): try: - # Go through all of the amplitudes we were given - df = [] - for a in np.atleast_1d(A): - df.append(F.describing_function(a)) - return np.array(df).reshape(np.shape(A)) + return np.vectorize(F.describing_function, otypes=[complex])(A) except NotImplementedError: # Drop through and do the numerical computation pass @@ -170,17 +164,20 @@ def describing_function( # See if this is a static nonlinearity (assume not, just in case) if not hasattr(F, '_isstatic') or not F._isstatic(): # Initialize any internal state by going through an initial cycle - [F(x) for x in np.atleast_1d(A).min() * sin_theta] + for x in np.atleast_1d(A).min() * sin_theta: + F(x) # ignore the result # Go through all of the amplitudes we were given - df = [] - for a in np.atleast_1d(A): + retdf = np.empty(np.shape(A), dtype=complex) + df = retdf # Access to the return array + df.shape = (-1, ) # as a 1D array + for i, a in enumerate(np.atleast_1d(A)): # Make sure we got a valid argument if a == 0: # Check to make sure the function has zero output with zero input if zero_check and np.squeeze(F(0.)) != 0: raise ValueError("function must evaluate to zero at zero") - df.append(1.) + df[i] = 1. continue elif a < 0: raise ValueError("cannot evaluate describing function for A < 0") @@ -195,10 +192,10 @@ def describing_function( df_real = (F_eval @ sin_theta) * scale # = M_1 \cos\phi / a df_imag = (F_eval @ cos_theta) * scale # = M_1 \sin\phi / a - df.append(df_real + 1j * df_imag) + df[i] = df_real + 1j * df_imag # Return the values in the same shape as they were requested - return np.array(df).reshape(np.shape(A)) + return retdf def describing_function_plot( @@ -437,16 +434,16 @@ def describing_function(self, A): return df_real + 1j * df_imag -# Backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) -class backlash_nonlinearity(DescribingFunctionNonlinearity): +# Friction-dominated backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) +class friction_backlash_nonlinearity(DescribingFunctionNonlinearity): """Backlash nonlinearity for use in describing function analysis - This class creates a nonlinear function representing a backlash - nonlinearity ,including the describing function for the nonlinearity. The - following call creates a nonlinear function suitable for describing - function analysis: + This class creates a nonlinear function representing a friction-dominated + backlash nonlinearity ,including the describing function for the + nonlinearity. The following call creates a nonlinear function suitable + for describing function analysis: - F = backlash_nonlinearity(b) + F = friction_backlash_nonlinearity(b) This function maintains an internal state representing the 'center' of a mechanism with backlash. If the new input is within `b/2` of the current @@ -457,7 +454,7 @@ class backlash_nonlinearity(DescribingFunctionNonlinearity): def __init__(self, b): # Create the describing function nonlinearity object - super(backlash_nonlinearity, self).__init__() + super(friction_backlash_nonlinearity, self).__init__() self.b = b # backlash distance self.center = 0 # current center position diff --git a/control/tests/descfcn_test.py b/control/tests/descfcn_test.py index 184227cb9..d26e2c67a 100644 --- a/control/tests/descfcn_test.py +++ b/control/tests/descfcn_test.py @@ -12,8 +12,8 @@ import numpy as np import control as ct import math -from control.descfcn import saturation_nonlinearity, backlash_nonlinearity, \ - relay_hysteresis_nonlinearity +from control.descfcn import saturation_nonlinearity, \ + friction_backlash_nonlinearity, relay_hysteresis_nonlinearity # Static function via a class @@ -84,15 +84,15 @@ def test_saturation_describing_function(satsys): df_anal = [satfcn.describing_function(a) for a in amprange] # Compute describing function for a static function - df_fcn = [ct.describing_function(saturation, a) for a in amprange] + df_fcn = ct.describing_function(saturation, amprange) np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) # Compute describing function for a describing function nonlinearity - df_fcn = [ct.describing_function(satfcn, a) for a in amprange] + df_fcn = ct.describing_function(satfcn, amprange) np.testing.assert_almost_equal(df_fcn, df_anal, decimal=3) # Compute describing function for a static I/O system - df_sys = [ct.describing_function(satsys, a) for a in amprange] + df_sys = ct.describing_function(satsys, amprange) np.testing.assert_almost_equal(df_sys, df_anal, decimal=3) # Compute describing function on an array of values @@ -109,13 +109,13 @@ class my_saturation(ct.DescribingFunctionNonlinearity): def __call__(self, x): return saturation(x) satfcn_nometh = my_saturation() - df_nometh = [ct.describing_function(satfcn_nometh, a) for a in amprange] + df_nometh = ct.describing_function(satfcn_nometh, amprange) np.testing.assert_almost_equal(df_nometh, df_anal, decimal=3) @pytest.mark.parametrize("fcn, amin, amax", [ [saturation_nonlinearity(1), 0, 10], - [backlash_nonlinearity(2), 1, 10], + [friction_backlash_nonlinearity(2), 1, 10], [relay_hysteresis_nonlinearity(1, 1), 3, 10], ]) def test_describing_function(fcn, amin, amax): @@ -161,7 +161,7 @@ def test_describing_function_plot(): # Multiple intersections H_multiple = H_simple * ct.tf(*ct.pade(5, 4)) * 4 omega = np.logspace(-1, 3, 50) - F_backlash = ct.descfcn.backlash_nonlinearity(1) + F_backlash = ct.descfcn.friction_backlash_nonlinearity(1) amp = np.linspace(0.6, 5, 50) xsects = ct.describing_function_plot(H_multiple, F_backlash, amp, omega) for a, w in xsects: diff --git a/examples/describing_functions.ipynb b/examples/describing_functions.ipynb index 5bdf888dd..7d090bf17 100644 --- a/examples/describing_functions.ipynb +++ b/examples/describing_functions.ipynb @@ -97,7 +97,7 @@ "metadata": {}, "source": [ "### Backlash nonlinearity\n", - "A backlash nonlinearity can be obtained using the `ct.backlash_nonlinearity` function. This function takes as is argument the size of the backlash region." + "A friction-dominated backlash nonlinearity can be obtained using the `ct.friction_backlash_nonlinearity` function. This function takes as is argument the size of the backlash region." ] }, { @@ -119,13 +119,13 @@ } ], "source": [ - "backlash = ct.backlash_nonlinearity(0.5)\n", + "backlash = ct.friction_backlash_nonlinearity(0.5)\n", "theta = np.linspace(0, 2*np.pi, 50)\n", "x = np.sin(theta)\n", "plt.plot(x, [backlash(z) for z in x])\n", "plt.xlabel(\"Input, x\")\n", "plt.ylabel(\"Output, y = backlash(x)\")\n", - "plt.title(\"Input/output map for a backlash nonlinearity\");" + "plt.title(\"Input/output map for a friction-dominated backlash nonlinearity\");" ] }, { @@ -365,7 +365,7 @@ "omega = np.logspace(-3, 3, 500)\n", "\n", "# Nonlinearity\n", - "F_backlash = ct.backlash_nonlinearity(1)\n", + "F_backlash = ct.friction_backlash_nonlinearity(1)\n", "amp = np.linspace(0.6, 5, 50)\n", "\n", "# Describing function plot\n", From 93d97cc18b224afd60185b67109d5142e7c4c9bc Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 6 Feb 2021 13:22:59 -0800 Subject: [PATCH 8/8] small fixes rebasing against master --- control/freqplot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control/freqplot.py b/control/freqplot.py index 03a7dadfb..0876f1dde 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -523,7 +523,8 @@ def gen_zero_centered_series(val_min, val_max, period): def nyquist_plot( syslist, omega=None, plot=True, omega_limits=None, omega_num=None, label_freq=0, color=None, mirror='--', arrowhead_length=0.1, - arrowhead_width=0.1, *args, **kwargs): """Nyquist plot for a system + arrowhead_width=0.1, *args, **kwargs): + """Nyquist plot for a system Plots a Nyquist plot for the system over a (optional) frequency range.