Skip to content

Commit 03183d1

Browse files
authored
Fix iosys._find_size to detect inconsistent system definitions (#402)
* fix iosys._find_size() * fix and rerun steering example notebook * allow legacy iosys._find_size(1, scalar)
1 parent 5cb38e0 commit 03183d1

File tree

3 files changed

+92
-43
lines changed

3 files changed

+92
-43
lines changed

control/iosys.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,16 +1745,23 @@ def linearize(sys, xeq, ueq=[], t=0, params={}, **kw):
17451745
return sys.linearize(xeq, ueq, t=t, params=params, **kw)
17461746

17471747

1748-
# Utility function to find the size of a system parameter
17491748
def _find_size(sysval, vecval):
1750-
if sysval is not None:
1751-
return sysval
1752-
elif hasattr(vecval, '__len__'):
1749+
"""Utility function to find the size of a system parameter
1750+
1751+
If both parameters are not None, they must be consistent.
1752+
"""
1753+
if hasattr(vecval, '__len__'):
1754+
if sysval is not None and sysval != len(vecval):
1755+
raise ValueError("Inconsistend information to determine size "
1756+
"of system component")
17531757
return len(vecval)
1754-
elif vecval is None:
1755-
return 0
1756-
else:
1757-
raise ValueError("Can't determine size of system component.")
1758+
# None or 0, which is a valid value for "a (sysval, ) vector of zeros".
1759+
if not vecval:
1760+
return 0 if sysval is None else sysval
1761+
elif sysval == 1:
1762+
# (1, scalar) is also a valid combination from legacy code
1763+
return 1
1764+
raise ValueError("Can't determine size of system component.")
17581765

17591766

17601767
# Convert a state space system into an input/output system (wrapper)

control/tests/iosys_test.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -286,19 +286,19 @@ def test_algebraic_loop(self):
286286
# Set up parameters for simulation
287287
T, U, X0 = self.T, self.U, self.X0
288288

289-
# Single nonlinear system - no states
290-
ios_t, ios_y = ios.input_output_response(nlios, T, U, X0)
289+
# Single nonlinear system - no states
290+
ios_t, ios_y = ios.input_output_response(nlios, T, U)
291291
np.testing.assert_array_almost_equal(ios_y, U*U, decimal=3)
292292

293293
# Composed nonlinear system (series)
294-
ios_t, ios_y = ios.input_output_response(nlios1 * nlios2, T, U, X0)
294+
ios_t, ios_y = ios.input_output_response(nlios1 * nlios2, T, U)
295295
np.testing.assert_array_almost_equal(ios_y, U**4, decimal=3)
296296

297297
# Composed nonlinear system (parallel)
298-
ios_t, ios_y = ios.input_output_response(nlios1 + nlios2, T, U, X0)
298+
ios_t, ios_y = ios.input_output_response(nlios1 + nlios2, T, U)
299299
np.testing.assert_array_almost_equal(ios_y, 2*U**2, decimal=3)
300300

301-
# Nonlinear system composed with LTI system (series)
301+
# Nonlinear system composed with LTI system (series) -- with states
302302
ios_t, ios_y = ios.input_output_response(
303303
nlios * lnios * nlios, T, U, X0)
304304
lti_t, lti_y, lti_x = ct.forced_response(linsys, T, U*U, X0)
@@ -323,7 +323,7 @@ def test_algebraic_loop(self):
323323
(1, (0, 0, -1))),
324324
0, 0
325325
)
326-
args = (iosys, T, U, X0)
326+
args = (iosys, T, U)
327327
self.assertRaises(RuntimeError, ios.input_output_response, *args)
328328

329329
# Algebraic loop due to feedthrough term
@@ -392,7 +392,7 @@ def test_neg(self):
392392
# Static nonlinear system
393393
nlios = ios.NonlinearIOSystem(None, \
394394
lambda t, x, u, params: u*u, inputs=1, outputs=1)
395-
ios_t, ios_y = ios.input_output_response(-nlios, T, U, X0)
395+
ios_t, ios_y = ios.input_output_response(-nlios, T, U)
396396
np.testing.assert_array_almost_equal(ios_y, -U*U, decimal=3)
397397

398398
# Linear system with input nonlinearity
@@ -807,6 +807,50 @@ def test_named_signals(self):
807807
np.testing.assert_array_almost_equal(ss_feedback.C, lin_feedback.C)
808808
np.testing.assert_array_almost_equal(ss_feedback.D, lin_feedback.D)
809809

810+
def test_named_signals_linearize_inconsistent(self):
811+
"""Mare sure that providing inputs or outputs not consistent with
812+
updfcn or outfcn fail
813+
"""
814+
815+
def updfcn(t, x, u, params):
816+
"""2 inputs, 2 states"""
817+
return np.array(
818+
np.dot(self.mimo_linsys1.A, np.reshape(x, (-1, 1)))
819+
+ np.dot(self.mimo_linsys1.B, np.reshape(u, (-1, 1)))
820+
).reshape(-1,)
821+
822+
def outfcn(t, x, u, params):
823+
"""2 states, 2 outputs"""
824+
return np.array(
825+
self.mimo_linsys1.C * np.reshape(x, (-1, 1))
826+
+ self.mimo_linsys1.D * np.reshape(u, (-1, 1))
827+
).reshape(-1,)
828+
829+
for inputs, outputs in [
830+
(('u[0]'), ('y[0]', 'y[1]')), # not enough u
831+
(('u[0]', 'u[1]', 'u[toomuch]'), ('y[0]', 'y[1]')),
832+
(('u[0]', 'u[1]'), ('y[0]')), # not enough y
833+
(('u[0]', 'u[1]'), ('y[0]', 'y[1]', 'y[toomuch]'))]:
834+
sys1 = ios.NonlinearIOSystem(updfcn=updfcn,
835+
outfcn=outfcn,
836+
inputs=inputs,
837+
outputs=outputs,
838+
states=self.mimo_linsys1.states,
839+
name='sys1')
840+
self.assertRaises(ValueError, sys1.linearize, [0, 0], [0, 0])
841+
842+
sys2 = ios.NonlinearIOSystem(updfcn=updfcn,
843+
outfcn=outfcn,
844+
inputs=('u[0]', 'u[1]'),
845+
outputs=('y[0]', 'y[1]'),
846+
states=self.mimo_linsys1.states,
847+
name='sys1')
848+
for x0, u0 in [([0], [0, 0]),
849+
([0, 0, 0], [0, 0]),
850+
([0, 0], [0]),
851+
([0, 0], [0, 0, 0])]:
852+
self.assertRaises(ValueError, sys2.linearize, x0, u0)
853+
810854
def test_lineariosys_statespace(self):
811855
"""Make sure that a LinearIOSystem is also a StateSpace object"""
812856
iosys_siso = ct.LinearIOSystem(self.siso_linsys)
@@ -931,7 +975,7 @@ def predprey(t, x, u, params={}):
931975
def pvtol(t, x, u, params={}):
932976
from math import sin, cos
933977
m = params.get('m', 4.) # kg, system mass
934-
J = params.get('J', 0.0475) # kg m^2, system inertia
978+
J = params.get('J', 0.0475) # kg m^2, system inertia
935979
r = params.get('r', 0.25) # m, thrust offset
936980
g = params.get('g', 9.8) # m/s, gravitational constant
937981
c = params.get('c', 0.05) # N s/m, rotational damping
@@ -946,7 +990,7 @@ def pvtol(t, x, u, params={}):
946990
def pvtol_full(t, x, u, params={}):
947991
from math import sin, cos
948992
m = params.get('m', 4.) # kg, system mass
949-
J = params.get('J', 0.0475) # kg m^2, system inertia
993+
J = params.get('J', 0.0475) # kg m^2, system inertia
950994
r = params.get('r', 0.25) # m, thrust offset
951995
g = params.get('g', 9.8) # m/s, gravitational constant
952996
c = params.get('c', 0.05) # N s/m, rotational damping

examples/steering.ipynb

Lines changed: 24 additions & 26 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)