Skip to content

Commit fa162ec

Browse files
committed
add OperatingPoint object + unit test
1 parent 7479353 commit fa162ec

File tree

2 files changed

+100
-1
lines changed

2 files changed

+100
-1
lines changed

control/nlsys.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
__all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys',
3434
'input_output_response', 'find_eqpt', 'linearize',
35-
'interconnect', 'connection_table']
35+
'interconnect', 'connection_table', 'find_operating_point']
3636

3737

3838
class NonlinearIOSystem(InputOutputSystem):
@@ -1663,6 +1663,60 @@ def ivp_rhs(t, x):
16631663
success=soln.success, message=message)
16641664

16651665

1666+
class OperatingPoint(object):
1667+
"""A class for representing the operating point of a nonlinear I/O system.
1668+
1669+
The ``OperatingPoint`` class stores the operating point of a nonlinear
1670+
system, which consists of the state and input for a nonlinear system.
1671+
The main use for this class is as the return object for the
1672+
:func:`find_operating_point` function.
1673+
1674+
Attributes
1675+
----------
1676+
xop : array
1677+
State vector at the operating point.
1678+
uop : array
1679+
Input vector at the operating point.
1680+
result : :class:`scipy.optimize.OptimizeResult`, optional
1681+
Result from the :func:`scipy.optimize.root` function, if available.
1682+
1683+
"""
1684+
def __init__(
1685+
self, xop, uop=None, yop=None, result=None,
1686+
return_y=False, return_result=False):
1687+
self.xop = xop
1688+
self.uop = uop
1689+
1690+
if yop is None and return_y and not return_result:
1691+
raise SystemError("return_y specified by no y0 value")
1692+
self.yop = yop
1693+
self.return_y = return_y
1694+
1695+
if result is None and return_result:
1696+
raise SystemError("return_result specified by no result value")
1697+
self.result = result
1698+
self.return_result = return_result
1699+
1700+
# Implement iter to allow assigning to a tuple
1701+
def __iter__(self):
1702+
if self.return_y and self.return_result:
1703+
return iter((self.xop, self.uop, self.yop, self.result))
1704+
elif self.return_y:
1705+
return iter((self.xop, self.uop, self.yop))
1706+
elif self.return_result:
1707+
return iter((self.xop, self.uop, self.result))
1708+
else:
1709+
return iter((self.xop, self.uop))
1710+
1711+
# Implement (thin) getitem to allow access via legacy indexing
1712+
def __getitem__(self, index):
1713+
return list(self.__iter__())[index]
1714+
1715+
# Implement (thin) len to emulate legacy return value
1716+
def __len__(self):
1717+
return len(list(self.__iter__()))
1718+
1719+
16661720
def find_operating_point(
16671721
sys, x0, u0=None, y0=None, t=0, params=None,
16681722
iu=None, iy=None, ix=None, idx=None, dx0=None, root_method=None,
@@ -1946,6 +2000,15 @@ def rootfun(z):
19462000
z = (x, u, sys._out(t, x, u))
19472001

19482002
# Return the result based on what the user wants and what we found
2003+
if return_result or result.success:
2004+
return OperatingPoint(
2005+
z[0], z[1], z[2], result, return_y, return_result)
2006+
else:
2007+
# Something went wrong, don't return anything
2008+
return OperatingPoint(
2009+
None, None, None, result, return_y, return_result)
2010+
2011+
# TODO: remove code when ready
19492012
if not return_y:
19502013
z = z[0:2] # Strip y from result if not desired
19512014
if return_result:

control/tests/iosys_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,42 @@ def test_find_eqpt(x0, ix, u0, iu, y0, iy, dx0, idx, dt, x_expect, u_expect):
20872087
np.testing.assert_allclose(np.array(ueq), u_expect, atol=1e-6)
20882088

20892089

2090+
# Test out new operating point version of find_eqpt
2091+
def test_find_operating_point():
2092+
dt = 1
2093+
sys = ct.NonlinearIOSystem(
2094+
eqpt_rhs, eqpt_out, dt=dt, states=3, inputs=2, outputs=2)
2095+
2096+
# Conditions that lead to no exact solution (from previous unit test)
2097+
x0 = 0; ix = None
2098+
u0 = [-1, 0]; iu = None
2099+
y0 = None; iy = None
2100+
dx0 = None; idx = None
2101+
2102+
# Default version: no equilibrium solution => returns None
2103+
op_point = ct.find_operating_point(
2104+
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx)
2105+
assert op_point.xop is None
2106+
assert op_point.uop is None
2107+
assert op_point.result.success is False
2108+
2109+
# Change the method to Levenberg-Marquardt (gives nearest point)
2110+
op_point = ct.find_operating_point(
2111+
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx,
2112+
root_method='lm')
2113+
assert op_point.xop is not None
2114+
assert op_point.uop is not None
2115+
assert op_point.result.success is True
2116+
2117+
# Make sure we get a solution if we ask for the result explicitly
2118+
op_point = ct.find_operating_point(
2119+
sys, x0, u0, y0, ix=ix, iu=iu, iy=iy, dx0=dx0, idx=idx,
2120+
return_result=True)
2121+
assert op_point.xop is not None
2122+
assert op_point.uop is not None
2123+
assert op_point.result.success is False
2124+
2125+
20902126
def test_iosys_sample():
20912127
csys = ct.rss(2, 1, 1)
20922128
dsys = csys.sample(0.1)

0 commit comments

Comments
 (0)