-
Notifications
You must be signed in to change notification settings - Fork 438
Describing functions #521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Describing functions #521
Conversation
e180716
to
e82e2af
Compare
…be unneccesary and to avoid a merge conbflict with python-control#521
control/iosys.py
Outdated
@@ -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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
statespace and xferfcn have methods is_static_gain()
I added. should we choose a standard conventiuon? I would lean toward isstatic()
because it is shorter and matches issiso()
,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree we should be uniform. I'm not sure that is_static_gain
() would make sense for a nonlinear system, so that is another reason to opt for isstatic()
.
Also: these are mainly internally used functions, so I think it is OK to stick with isstatic
/issiso
rather than is_static
/is_siso
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converted to _isstatic
in latest push since this really is an internal function, not intended to be seen by the user.
@sawyerbfuller: when we update is_static_gain
, we can either change to _isstatic
if all usage is internal or change to isstatic
(and update descfcn.py
for consistency).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed in #524
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try to review from describing_function_plot
on later this weekend.
control/descfcn.py
Outdated
|
||
# Class for nonlinearities with a built-in describing function | ||
class DescribingFunctionNonlinearity(): | ||
"""Base class for nonlinear functions with a describing function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Base class for nonlinear functions with a describing function | |
"""Base class for nonlinear systems with a describing function |
control/descfcn.py
Outdated
"""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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that have a analytically defined describing function (accessed via the | |
that have an analytically defined describing function (accessed via the |
control/descfcn.py
Outdated
# | ||
|
||
"""The :mod:~control.descfcn` module contains function for performing | ||
closed loop analysis of systems with static nonlinearities using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the static
terminology a little confusing, especially given that some of the describing functions declared here, e.g. hysteresis, return False
in their _isstatic
methods. Maybe "non-linearities with frequency-independent describing functions", though that is rather long-winded.
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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about: "Subclasses should override the describing_function
, __call__
, and _isstatic
methods."
It looks like __call__
's inputs and outputs must be scalar, since __call__
is allowed to change instance memory, so the sequence of calls matters.
Can describing_function
be called with a numpy.ndarray
of amplitude values? This seems like a reasonable thing to want to do.
If I understand correctly, if __call__
updates any instance state, then _isstatic
must return False
-- it would be good to mention that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the documentation a bit to address the comments above.
In terms of calling DescribingFunctionNonlinearity.description_function
with an array: the intent is that this is a low level function that just evaluates the describing function at a scalar value. The describing_function
function has the code to evaluate on an array.
|
||
""" | ||
# If there is an analytical solution, trying using that first | ||
if try_method and hasattr(F, 'describing_function'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the hasattr
check will succeed for any base class of DescribingFunctionNonlinearity
- shouldn't you also catch the NotImplementedError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed.
control/descfcn.py
Outdated
# | ||
|
||
# Evaluate over a full range of angles | ||
theta = np.linspace(0, 2*np.pi, num_points) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not something I'm not at all sure about: should this omit the final point, so it's more like a discrete Fourier transform?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. We are approximating the integral by a discrete sum with the assumption that the function is constant on each small interval. But this means that we don't want to include the value at 2pi (since we already captured the segment just before 2pi in the previous evaluation).
control/descfcn.py
Outdated
# 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this is evaluating the function twice over the sinusoid (assume that means two periods of sinusoid).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left over from an earlier iteration (before I implemented _isstatic
). Fixed.
control/descfcn.py
Outdated
# | ||
# 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# To compute this, we compute F(\theta) for \theta between 0 and 2 \pi, | |
# To compute this, we compute F(A\sin\theta) for \theta between 0 and 2 \pi, |
control/descfcn.py
Outdated
# | ||
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# \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) \sin\theta d\theta = M_1 \pi \cos\phi |
control/descfcn.py
Outdated
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# \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) \cos\theta d\theta = M_1 \pi \sin\phi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Searching for the intersections will be O(m * n).
We could look libraries we can pull in to find the intersection of H(jw) and N(A). I googled "numpy polyline intersection" and found pointers to https://pypi.org/project/Shapely/, but that may be over the top.
We could also look at using scipy.optimize.root
, though I don't see an obvious way to bound the domain of search. And we'd have to handle the multiple roots ourselves.
When N(A) is strictly real (saturation, deadzone, relay), we could probably specialize the solver.
control/descfcn.py
Outdated
self.ub = ub | ||
|
||
def __call__(self, x): | ||
return np.maximum(self.lb, np.minimum(x, self.ub)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return np.maximum(self.lb, np.minimum(x, self.ub)) | |
return np.clip(x, self.lb, self.ub) |
control/freqplot.py
Outdated
@@ -508,7 +508,7 @@ def gen_zero_centered_series(val_min, val_max, period): | |||
|
|||
def nyquist_plot(syslist, omega=None, plot=True, label_freq=0, | |||
arrowhead_length=0.1, arrowhead_width=0.1, | |||
color=None, *args, **kwargs): | |||
mirror='--', color=None, *args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new argument should be documented. Are we going to allow None
for no mirror? Below you checked for False
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated documentation. Should either be a linestyle or False if you don't want to plot the mirror image at all. Setting it to (explicitly) to None will generate an error (from matplotlib).
if self.lb <= A and A <= self.ub: | ||
return 1. | ||
else: | ||
alpha, beta = math.asin(self.ub/A), math.asin(-self.lb/A) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, the expression in Slotine & Li, and also https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-30-estimation-and-control-of-aerospace-systems-spring-2004/readings/gelb_ch2_ocr.pdf and https://ocw.mit.edu/resources/res-6-010-electronic-feedback-systems-spring-2013/textbook/MITRES_6-010S13_chap06.pdf are different to what you have here, but they may be equivalent. All these references restrict themselves to saturation with odd symmetry, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's equivalent (I checked, but also the unit tests check this).
|
||
# Backlash nonlinearity (#48 in Gelb and Vander Velde, 1968) | ||
class backlash_nonlinearity(DescribingFunctionNonlinearity): | ||
"""Backlash nonlinearity for use in describing function analysis |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Friction-controlled backlash? (https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-30-estimation-and-control-of-aerospace-systems-spring-2004/readings/gelb_ch2_ocr.pdf p. 68?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw that, but couldn't figure out what the "friction-controlled" had to do with it exactly. It seemed to me that you just had backlash as in a gear and so the output just followed the input (modulo the backlash action).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's not important, but I remember a colleague always talking about the two backlash types (friction-controlled and inertia-controlled), and I thought it was standard nomenclature. The inertia-controlled case is discussed in the reference above immediately after friction-controlled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I was looking at something different (probably just the table in Appendix B, which didn't have the explanation). I'll update.
control/descfcn.py
Outdated
# Convert input to an array | ||
x_array = np.array(x) | ||
|
||
y = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's an inconsistency here: you've allowed a vector input for this __call__
, but not the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch.
control/tests/descfcn_test.py
Outdated
|
||
|
||
# Static function via a class | ||
class saturation_class(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class saturation_class(): | |
class saturation_class: |
control/tests/descfcn_test.py
Outdated
np.testing.assert_array_equal(satsys([0.]), [0.]) | ||
|
||
# Test SIMO nonlinearity | ||
def _simofcn(t, x, u, params={}): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def _simofcn(t, x, u, params={}): | |
def _simofcn(t, x, u, params): |
control/tests/descfcn_test.py
Outdated
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]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np.testing.assert_array_equal(miso_sys([0, 0]), [0]) |
|
||
|
||
# Test saturation describing function in multiple ways | ||
def test_saturation_describing_function(satsys): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mean to test with the function saturation
here, but this tests saturation_class
twice.
doc/descfcn.rst
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e^{j\omega t}
?
aba5b43
to
f5bca43
Compare
f5bca43
to
93d97cc
Compare
This PR adds a basic describing function capability, mainly captured in the new
describing_function_plot
command. Almost all of the new code is in thedescfcn.py
file, with a few changes elsewhere to support some of the patterns. There is a Jupyter notebook demonstrating the use of the code.Additional changes:
mirror
keyword tonyquist_plot
that sets the line style used for plotting the complementary portion of the Nyquist plot. This defaults to--
, which matches the FBS convention._isstatic
method to theNonlinearIOSystem
class that allows passing a nonlinear I/O object as the argument for computing describing functions.__call__
method to theNonlinearIOSystem
class that direct calling of a static nonlinear input/output function (returning the value of the function at the given input).