-
Notifications
You must be signed in to change notification settings - Fork 438
Update find_eqpt to find_operating_point, adding root_method + better docs #1054
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
Update find_eqpt to find_operating_point, adding root_method + better docs #1054
Conversation
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 improvements! I made several comments to consider before merging.
I ran the example that originally motivated this pull request, and I confirmed the result is as expected after adding the new argument root_method='lm'
.
Note that other files in examples/ besides cruise-control.py use find_eqpt()
. However, it is now an alias for find_operating_point()
without any indication of being deprecated, so OK to leave the other examples for now.
control/tests/iosys_test.py
Outdated
np.testing.assert_allclose(linsys_orig.C, linsys_oppt.C) | ||
np.testing.assert_allclose(linsys_orig.D, linsys_oppt.D) | ||
|
||
# Call find_operating point with method and keyword arguments |
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.
# Call find_operating point with method and keyword arguments | |
# Call find_operating_point with method and keyword arguments |
control/nlsys.py
Outdated
@@ -92,7 +93,7 @@ class NonlinearIOSystem(InputOutputSystem): | |||
generic name <sys[id]> is generated with a unique integer id. | |||
|
|||
params : dict, optional | |||
Parameter values for the system. Passed to the evaluation functions | |||
Parameter values for the system. Passed to the evaluation functions |
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 indentation step size is 4 spaces, but this change is to 3 spaces. Was this an accident?
control/nlsys.py
Outdated
|
||
""" | ||
def __init__( | ||
self, states, inputs=None, outputs=None, result=None, |
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.
self, states, inputs=None, outputs=None, result=None, | |
self, states, inputs, outputs=None, result=None, |
inputs
parameter is not optional.
control/tests/iosys_test.py
Outdated
@@ -2087,6 +2088,101 @@ def test_find_eqpt(x0, ix, u0, iu, y0, iy, dx0, idx, dt, x_expect, u_expect): | |||
np.testing.assert_allclose(np.array(ueq), u_expect, atol=1e-6) | |||
|
|||
|
|||
# Test out new operating point version of find_eqpt | |||
# TODO: add return_)y tests |
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 is a test below (function test_operating_point
) that uses return_y
. Is something else intended with this TODO?
control/nlsys.py
Outdated
return_y : bool, optional | ||
If True, return the value of output at the equilibrium point. | ||
fixed at `dx0` in searching for an operating point. | ||
root_method : str, optonal |
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.
root_method : str, optonal | |
root_method : str, optional |
def find_operating_point( | ||
sys, x0, u0=None, y0=None, t=0, params=None, iu=None, iy=None, | ||
ix=None, idx=None, dx0=None, root_method=None, root_kwargs=None, | ||
return_outputs=None, return_result=None, **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.
return_outputs=None, return_result=None, **kwargs): | |
return_outputs=False, return_result=False, **kwargs): |
return_outputs
and return_result
are not used in a way where the distinction between None
and False
matters. I recommend to write the default value in the expected type, i.e., False
, because the docstring shows these as bool, optional
without explicitly stating the default value, so users will look at the function signature and may be confused if it is None
.
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 needs to be None
in order for the legacy keyword handling to work: if both return_y
and return_outputs
are specified, _process_legacy_keyword
will raise an exception. Thus with this change, setting the default value of return_outputs
to False would mean that if you call find_operating_point
with return_y
, you get an exception instead of a warning.
control/nlsys.py
Outdated
iu=None, iy=None, ix=None, idx=None, dx0=None, | ||
return_y=False, return_result=False): | ||
"""Find the equilibrium point for an input/output system. | ||
class OperatingPoint(object): |
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.
Python 3 has classes inherit from object
by default, so writing explicitly is not required: https://docs.python.org/3/reference/compound_stmts.html#class-definitions
However, I am not aware of a style recommendation. black
does not add (object)
and does not remove it, but the standard Python tutorial gives examples without explicit object
: https://docs.python.org/3/tutorial/classes.html#class-objects
I forgot to make this comment on a recent pull request that I reviewed where a new class was defined. Anyway, we can keep explicit object
inheritance for consistency with existing code and consider removing it later to be more concise.
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.
Since we are using Python 3 and that only supports new classes, I think we should move to not inheriting from object
(which is how things are done in the official documentation).
control/nlsys.py
Outdated
self.inputs = inputs | ||
|
||
if outputs is None and return_outputs and not return_result: | ||
raise SystemError("return_outputs specified by no y0 value") |
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.
raise SystemError("return_outputs specified by no y0 value") | |
raise SystemError("return_outputs specified but no y0 value") |
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.
Here and below are both non-standard uses of SystemError, which normally is for "when the [Python] interpreter finds an internal error". I recommend ValueError instead, which is also used elsewhere in this module for inconsistent arguments.
control/nlsys.py
Outdated
self.return_outputs = return_outputs | ||
|
||
if result is None and return_result: | ||
raise SystemError("return_result specified by no result value") |
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.
raise SystemError("return_result specified by no result value") | |
raise SystemError("return_result specified but no result value") |
examples/cruise-control.py
Outdated
@@ -347,7 +347,7 @@ def cruise_plot(sys, t, y, label=None, t_hill=None, vref=20, antiwindup=False, | |||
|
|||
# Compute the equilibrium throttle setting for the desired speed (solve for x | |||
# and u given the gear, slope, and desired output velocity) | |||
X0, U0, Y0 = ct.find_eqpt( | |||
X0, U0, Y0 = ct.find_operating_point( | |||
cruise_pi, [vref[0], 0], [vref[0], gear[0], theta0[0]], | |||
y0=[0, vref[0]], iu=[1, 2], iy=[1], return_y=True) |
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.
y0=[0, vref[0]], iu=[1, 2], iy=[1], return_y=True) | |
y0=[0, vref[0]], iu=[1, 2], iy=[1], return_outputs=True) |
Update the argument to avoid a FutureWarning
being raised in the example.
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.
Similarly, return_y=True
in examples/cruise.ipynb should be updated, too.
This PR is motivated by discussion #1053 in which a situation the current implementation of
find_eqpt
does not find a good solution due to the problem being over/underconstrained. The main functional change is this PR is to help address that by adding aroot_method
method that allows a least squares solution to be obtained.In addition, while looking through this (fairly old) code, it seemed worth making some additional changes:
find_eqpt
tofind_operating_point
to be more consistent with python-control naming conventions (and the fact that an operating point is not always an equilibrium point.find_operating_point
from a tuple of various lengths (depending on parameter settings) to aOperatingPoint
object, which contains the states, inputs, and outputs corresponding to the operating point, along with the result of the call to thescipy.minimize.root
function.root_method
androot_kwargs
parameters to allow customization of the root finding method.find_eqpt
still works, as well asreturn_y
(nowreturn_outputs
, with legacy processing ofreturn_y
) andreturn_result
. In addition, the return object can be accessed as a tuple for consistency with the previous return signature.In the context of #1053, a solution to the issue there can be obtained by calling
find_eqpt
(nowfind_operating_point
) withroot_method='lm'
.