Skip to content

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

Merged
merged 8 commits into from
Nov 14, 2024
2 changes: 2 additions & 0 deletions control/matlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@
from ..dtime import c2d
from ..sisotool import sisotool
from ..stochsys import lqe, dlqe
from ..nlsys import find_operating_point

# Functions that are renamed in MATLAB
pole, zero = poles, zeros
freqresp = frequency_response
trim = find_operating_point
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

control.matlab does not claim perfect compatibility, but note that Matlab trim has a different parameter order than find_operating_point() after y0:

trim('sys',x0,u0,y0,ix,iu,iy,dx0,idx,options,t)


# Import functions specific to Matlab compatibility package
from .timeresp import *
Expand Down
269 changes: 198 additions & 71 deletions control/nlsys.py

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions control/phaseplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \
_process_ax_keyword, _update_plot_title
from .exception import ControlNotImplemented
from .nlsys import NonlinearIOSystem, find_eqpt, input_output_response
from .nlsys import NonlinearIOSystem, find_operating_point, \
input_output_response

__all__ = ['phase_plane_plot', 'phase_plot', 'box_grid']

Expand Down Expand Up @@ -853,7 +854,7 @@ def _find_equilpts(sys, points, params=None):
equilpts = []
for i, x0 in enumerate(points):
# Look for an equilibrium point near this point
xeq, ueq = find_eqpt(sys, x0, 0, params=params)
xeq, ueq = find_operating_point(sys, x0, 0, params=params)

if xeq is None:
continue # didn't find anything
Expand Down
1 change: 1 addition & 0 deletions control/tests/docstrings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
control.create_estimator_iosystem: ['state_labels'], # deprecated
control.bode_plot: ['sharex', 'sharey', 'margin_info'], # deprecated
control.eigensys_realization: ['arg'], # quasi-positional
control.find_operating_point: ['method'], # internal use
}

# Decide on the level of verbosity (use -rP when running pytest)
Expand Down
99 changes: 97 additions & 2 deletions control/tests/iosys_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

import re
import warnings
import pytest
from math import sqrt

import numpy as np
from math import sqrt
import pytest
import scipy

import control as ct

Expand Down Expand Up @@ -2087,6 +2088,100 @@ 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
def test_find_operating_point():
dt = 1
sys = ct.NonlinearIOSystem(
eqpt_rhs, eqpt_out, dt=dt, states=3, inputs=2, outputs=2)

# Conditions that lead to no exact solution (from previous unit test)
x0 = 0; ix = None
u0 = [-1, 0]; iu = None
y0 = None; iy = None
dx0 = None; idx = None

# Default version: no equilibrium solution => returns None
op_point = ct.find_operating_point(
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx)
assert op_point.states is None
assert op_point.inputs is None
assert op_point.result.success is False

# Change the method to Levenberg-Marquardt (gives nearest point)
op_point = ct.find_operating_point(
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx,
root_method='lm')
assert op_point.states is not None
assert op_point.inputs is not None
assert op_point.result.success is True

# Make sure we get a solution if we ask for the result explicitly
op_point = ct.find_operating_point(
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx,
return_result=True)
assert op_point.states is not None
assert op_point.inputs is not None
assert op_point.result.success is False

# Check to make sure unknown keywords are caught
with pytest.raises(TypeError, match="unrecognized keyword"):
ct.find_operating_point(sys, x0, u0, unknown=None)


def test_operating_point():
dt = 1
sys = ct.NonlinearIOSystem(
eqpt_rhs, eqpt_out, dt=dt, states=3, inputs=2, outputs=2)

# Find the operating point near the origin
op_point = ct.find_operating_point(sys, 0, 0)

# Linearize the old fashioned way
linsys_orig = ct.linearize(sys, op_point.states, op_point.inputs)

# Linearize around the operating point
linsys_oppt = ct.linearize(sys, op_point)

np.testing.assert_allclose(linsys_orig.A, linsys_oppt.A)
np.testing.assert_allclose(linsys_orig.B, linsys_oppt.B)
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
op_point = ct.find_operating_point(
sys, 0, 0, root_method='lm', root_kwargs={'tol': 1e-6})

# Make sure we can get back the right arguments in a tuple
op_point = ct.find_operating_point(sys, 0, 0, return_outputs=True)
assert len(op_point) == 3
assert isinstance(op_point[0], np.ndarray)
assert isinstance(op_point[1], np.ndarray)
assert isinstance(op_point[2], np.ndarray)

with pytest.warns(FutureWarning, match="return_outputs"):
op_point = ct.find_operating_point(sys, 0, 0, return_y=True)
assert len(op_point) == 3
assert isinstance(op_point[0], np.ndarray)
assert isinstance(op_point[1], np.ndarray)
assert isinstance(op_point[2], np.ndarray)

# Make sure we can get back the right arguments in a tuple
op_point = ct.find_operating_point(sys, 0, 0, return_result=True)
assert len(op_point) == 3
assert isinstance(op_point[0], np.ndarray)
assert isinstance(op_point[1], np.ndarray)
assert isinstance(op_point[2], scipy.optimize.OptimizeResult)

# Make sure we can get back the right arguments in a tuple
op_point = ct.find_operating_point(
sys, 0, 0, return_result=True, return_outputs=True)
assert len(op_point) == 4
assert isinstance(op_point[0], np.ndarray)
assert isinstance(op_point[1], np.ndarray)
assert isinstance(op_point[2], np.ndarray)
assert isinstance(op_point[3], scipy.optimize.OptimizeResult)


def test_iosys_sample():
csys = ct.rss(2, 1, 1)
dsys = csys.sample(0.1)
Expand Down
3 changes: 3 additions & 0 deletions control/tests/kwargs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import control.tests.frd_test as frd_test
import control.tests.freqplot_test as freqplot_test
import control.tests.interconnect_test as interconnect_test
import control.tests.iosys_test as iosys_test
import control.tests.optimal_test as optimal_test
import control.tests.statefbk_test as statefbk_test
import control.tests.stochsys_test as stochsys_test
Expand Down Expand Up @@ -252,6 +253,8 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
'dlqr': test_unrecognized_kwargs,
'drss': test_unrecognized_kwargs,
'feedback': test_unrecognized_kwargs,
'find_eqpt': iosys_test.test_find_operating_point,
'find_operating_point': iosys_test.test_find_operating_point,
'flatsys.flatsys': test_unrecognized_kwargs,
'frd': frd_test.TestFRD.test_unrecognized_keyword,
'gangof4': test_matplotlib_kwargs,
Expand Down
2 changes: 1 addition & 1 deletion doc/control.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Nonlinear system support
:toctree: generated/

describing_function
find_eqpt
find_operating_point
linearize
input_output_response
summing_junction
Expand Down
6 changes: 3 additions & 3 deletions doc/conventions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,10 @@ element of the `control.config.defaults` dictionary::

ct.config.defaults['module.parameter'] = value

The `~control.config.set_defaults` function can also be used to set multiple
configuration parameters at the same time::
The :func:`~control.set_defaults` function can also be used to
set multiple configuration parameters at the same time::

ct.config.set_defaults('module', param1=val1, param2=val2, ...]
ct.set_defaults('module', param1=val1, param2=val2, ...]

Finally, there are also functions available set collections of variables based
on standard configurations.
Expand Down
2 changes: 1 addition & 1 deletion doc/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ they are not already present.
.. note::
Mixing packages from conda-forge and the default conda channel
can sometimes cause problems with dependencies, so it is usually best to
instally NumPy, SciPy, and Matplotlib from conda-forge as well.
install NumPy, SciPy, and Matplotlib from conda-forge as well.

To install using pip::

Expand Down
19 changes: 10 additions & 9 deletions doc/iosys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ function::
resp = ct.input_output_response(io_sys, T, U, X0, params)
t, y, x = resp.time, resp.outputs, resp.states

An input/output system can be linearized around an equilibrium point to obtain
a :class:`~control.StateSpace` linear system. Use the
:func:`~control.find_eqpt` function to obtain an equilibrium point and the
:func:`~control.linearize` function to linearize about that equilibrium point::
An input/output system can be linearized around an equilibrium point
to obtain a :class:`~control.StateSpace` linear system. Use the
:func:`~control.find_operating_point` function to obtain an
equilibrium point and the :func:`~control.linearize` function to
linearize about that equilibrium point::

xeq, ueq = ct.find_eqpt(io_sys, X0, U0)
xeq, ueq = ct.find_operating_point(io_sys, X0, U0)
ss_sys = ct.linearize(io_sys, xeq, ueq)

Input/output systems are automatically created for state space LTI systems
Expand Down Expand Up @@ -123,9 +124,8 @@ system and computing the linearization about that point.

.. code-block:: python

eqpt = ct.find_eqpt(io_predprey, X0, 0)
xeq = eqpt[0] # choose the nonzero equilibrium point
lin_predprey = ct.linearize(io_predprey, xeq, 0)
eqpt = ct.find_operating_point(io_predprey, X0, 0)
lin_predprey = ct.linearize(io_predprey, eqpt)

We next compute a controller that stabilizes the equilibrium point using
eigenvalue placement and computing the feedforward gain using the number of
Expand Down Expand Up @@ -544,11 +544,12 @@ Module classes and functions
~control.InterconnectedSystem
~control.LinearICSystem
~control.NonlinearIOSystem
~control.OperatingPoint

.. autosummary::
:toctree: generated/

~control.find_eqpt
~control.find_operating_point
~control.interconnect
~control.input_output_response
~control.linearize
Expand Down
6 changes: 3 additions & 3 deletions examples/cruise-control.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def motor_torque(omega, params={}):

for m in (1200, 1600, 2000):
# Compute the equilibrium state for the system
X0, U0 = ct.find_eqpt(
X0, U0 = ct.find_operating_point(
cruise_tf, [0, vref[0]], [vref[0], gear[0], theta0[0]],
iu=[1, 2], y0=[vref[0], 0], iy=[0], params={'m': m})

Expand Down Expand Up @@ -347,9 +347,9 @@ 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)
y0=[0, vref[0]], iu=[1, 2], iy=[1], return_outputs=True)

# Now simulate the effect of a hill at t = 5 seconds
plt.figure()
Expand Down
2 changes: 1 addition & 1 deletion examples/cruise.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@
"# Compute the equilibrium throttle setting for the desired speed\n",
"X0, U0, Y0 = ct.find_eqpt(\n",
" cruise_pi, [vref[0], 0], [vref[0], gear[0], theta0[0]],\n",
" y0=[0, vref[0]], iu=[1, 2], iy=[1], return_y=True)\n",
" y0=[0, vref[0]], iu=[1, 2], iy=[1], return_outputs=True)\n",
"\n",
"# Now simulate the effect of a hill at t = 5 seconds\n",
"plt.figure()\n",
Expand Down
Loading