Skip to content

Commit 36263d8

Browse files
committed
improve error messages for inconsistent size info in nlsys + small bug fix
1 parent c568c59 commit 36263d8

2 files changed

Lines changed: 74 additions & 18 deletions

File tree

control/nlsys.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,8 @@ def linearize(self, x0, u0, t=0, params=None, eps=1e-6,
537537
u0 = _concatenate_list_elements(u0, 'u0')
538538

539539
# Figure out dimensions if they were not specified.
540-
nstates = _find_size(self.nstates, x0, "states")
541-
ninputs = _find_size(self.ninputs, u0, "inputs")
540+
nstates = _find_size(self.nstates, x0, "x0")
541+
ninputs = _find_size(self.ninputs, u0, "u0")
542542

543543
# Convert x0, u0 to arrays, if needed
544544
if np.isscalar(x0):
@@ -1468,7 +1468,8 @@ def input_output_response(
14681468
# Use the input time points as the output time points
14691469
t_eval = T
14701470

1471-
# If we were passed a list of input, concatenate them (w/ broadcast)
1471+
# If we were passed a list of inputs, concatenate them (w/ broadcast)
1472+
# TODO: call _concatenate_list_elements
14721473
if isinstance(U, (tuple, list)) and len(U) != ntimepts:
14731474
U_elements = []
14741475
for i, u in enumerate(U):
@@ -1492,11 +1493,21 @@ def input_output_response(
14921493
# Save the newly created input vector
14931494
U = np.vstack(U_elements)
14941495

1496+
# Figure out the number of inputs
1497+
# TODO: call _concatenate_list_elements?
1498+
if sys.ninputs is None:
1499+
if isinstance(U, np.ndarray):
1500+
ninputs = U.shape[0] if U.size > 1 else U.size
1501+
else:
1502+
ninputs = 1
1503+
else:
1504+
ninputs = sys.ninputs
1505+
14951506
# Make sure the input has the right shape
1496-
if sys.ninputs is None or sys.ninputs == 1:
1507+
if ninputs is None or ninputs == 1:
14971508
legal_shapes = [(ntimepts,), (1, ntimepts)]
14981509
else:
1499-
legal_shapes = [(sys.ninputs, ntimepts)]
1510+
legal_shapes = [(ninputs, ntimepts)]
15001511

15011512
U = _check_convert_array(
15021513
U, legal_shapes, 'Parameter ``U``: ', squeeze=False)
@@ -1522,15 +1533,19 @@ def input_output_response(
15221533
X0 = _check_convert_array(
15231534
X0, [(nstates,), (nstates, 1)], 'Parameter ``X0``: ', squeeze=True)
15241535

1536+
# Update the parameter values (prior to evaluating outfcn)
1537+
sys._update_params(params)
1538+
15251539
# Figure out the number of outputs
1526-
if sys.noutputs is None:
1527-
# Evaluate the output function to find number of outputs
1528-
noutputs = np.shape(sys._out(T[0], X0, U[:, 0]))[0]
1540+
if sys.outfcn is None:
1541+
noutputs = nstates if sys.noutputs is None else sys.noutputs
15291542
else:
1530-
noutputs = sys.noutputs
1543+
noutputs = np.shape(sys._out(T[0], X0, U[:, 0]))[0]
15311544

1532-
# Update the parameter values
1533-
sys._update_params(params)
1545+
if sys.noutputs is not None and sys.noutputs != noutputs:
1546+
raise RuntimeError(
1547+
f"inconsistent size of outputs; system specified {sys.noutputs}, "
1548+
f"output function returned {noutputs}")
15341549

15351550
#
15361551
# Define a function to evaluate the input at an arbitrary time
@@ -1737,9 +1752,9 @@ def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None,
17371752
from scipy.optimize import root
17381753

17391754
# Figure out the number of states, inputs, and outputs
1740-
nstates = _find_size(sys.nstates, x0, "states")
1741-
ninputs = _find_size(sys.ninputs, u0, "inputs")
1742-
noutputs = _find_size(sys.noutputs, y0, "outputs")
1755+
nstates = _find_size(sys.nstates, x0, "x0")
1756+
ninputs = _find_size(sys.ninputs, u0, "u0")
1757+
noutputs = _find_size(sys.noutputs, y0, "y0")
17431758

17441759
# Convert x0, u0, y0 to arrays, if needed
17451760
if np.isscalar(x0):
@@ -1982,23 +1997,24 @@ def linearize(sys, xeq, ueq=None, t=0, params=None, **kw):
19821997
return sys.linearize(xeq, ueq, t=t, params=params, **kw)
19831998

19841999

1985-
def _find_size(sysval, vecval, label):
2000+
def _find_size(sysval, vecval, name="system component"):
19862001
"""Utility function to find the size of a system parameter
19872002
19882003
If both parameters are not None, they must be consistent.
19892004
"""
19902005
if hasattr(vecval, '__len__'):
19912006
if sysval is not None and sysval != len(vecval):
19922007
raise ValueError(
1993-
f"inconsistent information for number of {label}")
2008+
f"inconsistent information to determine size of {name}; "
2009+
f"expected {sysval} values, received {len(vecval)}")
19942010
return len(vecval)
19952011
# None or 0, which is a valid value for "a (sysval, ) vector of zeros".
19962012
if not vecval:
19972013
return 0 if sysval is None else sysval
19982014
elif sysval == 1:
19992015
# (1, scalar) is also a valid combination from legacy code
20002016
return 1
2001-
raise ValueError(f"can't determine number of {label}")
2017+
raise ValueError(f"can't determine size of {name}")
20022018

20032019

20042020
# Function to create an interconnected system

control/tests/iosys_test.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ def test_operand_badtype(self, C, op):
14221422
def test_neg_badsize(self):
14231423
# Create a system of unspecified size
14241424
sys = ct.NonlinearIOSystem(lambda t, x, u, params: -x)
1425-
with pytest.raises(ValueError, match="Can't determine"):
1425+
with pytest.raises(ValueError, match="Can't determine number"):
14261426
-sys
14271427

14281428
def test_bad_signal_list(self):
@@ -2077,6 +2077,7 @@ def test_find_eqpt(x0, ix, u0, iu, y0, iy, dx0, idx, dt, x_expect, u_expect):
20772077
np.testing.assert_allclose(np.array(xeq), x_expect, atol=1e-6)
20782078
np.testing.assert_allclose(np.array(ueq), u_expect, atol=1e-6)
20792079

2080+
20802081
def test_iosys_sample():
20812082
csys = ct.rss(2, 1, 1)
20822083
dsys = csys.sample(0.1)
@@ -2087,3 +2088,42 @@ def test_iosys_sample():
20872088
dsys = ct.sample_system(csys, 0.1)
20882089
assert isinstance(dsys, ct.StateSpace)
20892090
assert dsys.dt == 0.1
2091+
2092+
2093+
# Make sure that we can determine system sizes automatically
2094+
def test_find_size():
2095+
# Create a nonlinear system with no size information
2096+
sys = ct.nlsys(
2097+
lambda t, x, u, params: -x + u,
2098+
lambda t, x, u, params: x[:1])
2099+
2100+
# Run a simulation with size set by parameters
2101+
timepts = np.linspace(0, 1)
2102+
resp = ct.input_output_response(sys, timepts, [0, 1], X0=[0, 0])
2103+
assert resp.states.shape[0] == 2
2104+
assert resp.inputs.shape[0] == 2
2105+
assert resp.outputs.shape[0] == 1
2106+
2107+
#
2108+
# Make sure we get warnings if things are inconsistent
2109+
#
2110+
2111+
# Define a system of fixed size
2112+
sys = ct.nlsys(
2113+
lambda t, x, u, params: -x + u,
2114+
lambda t, x, u, params: x[:1],
2115+
inputs=2, states=2)
2116+
2117+
with pytest.raises(ValueError, match="inconsistent .* size of X0"):
2118+
resp = ct.input_output_response(sys, timepts, [0, 1], X0=[0, 0, 1])
2119+
2120+
with pytest.raises(ValueError, match=".*U.* Wrong shape"):
2121+
resp = ct.input_output_response(sys, timepts, [0, 1, 2], X0=[0, 0])
2122+
2123+
with pytest.raises(RuntimeError, match="inconsistent size of outputs"):
2124+
sys = ct.nlsys(
2125+
lambda t, x, u, params: -x + u,
2126+
lambda t, x, u, params: x[:1],
2127+
inputs=2, states=2, outputs=2)
2128+
resp = ct.input_output_response(sys, timepts, [0, 1], X0=[0, 0])
2129+

0 commit comments

Comments
 (0)