Skip to content

make _convert_to_statespace properly pass signal and system names #884

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 3 commits into from
Jun 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ class StateSpace(LTI):

The StateSpace class is used to represent state-space realizations of
linear time-invariant (LTI) systems:

.. math::

dx/dt &= A x + B u \\
y &= C x + D u

Expand Down Expand Up @@ -1561,7 +1561,8 @@ def _convert_to_statespace(sys):
return StateSpace(
ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
ssout[3][:sys.noutputs, :states], ssout[4], sys.dt,
inputs=sys.input_labels, outputs=sys.output_labels)
inputs=sys.input_labels, outputs=sys.output_labels,
name=sys.name)
except ImportError:
# No Slycot. Scipy tf->ss can't handle MIMO, but static
# MIMO is an easy special case we can check for here
Expand All @@ -1574,7 +1575,9 @@ def _convert_to_statespace(sys):
for i, j in itertools.product(range(sys.noutputs),
range(sys.ninputs)):
D[i, j] = sys.num[i][j][0] / sys.den[i][j][0]
return StateSpace([], [], [], D, sys.dt)
return StateSpace([], [], [], D, sys.dt,
inputs=sys.input_labels, outputs=sys.output_labels,
name=sys.name)
else:
if sys.ninputs != 1 or sys.noutputs != 1:
raise TypeError("No support for MIMO without slycot")
Expand All @@ -1586,7 +1589,7 @@ def _convert_to_statespace(sys):
sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
return StateSpace(
A, B, C, D, sys.dt, inputs=sys.input_labels,
outputs=sys.output_labels)
outputs=sys.output_labels, name=sys.name)

elif isinstance(sys, FrequencyResponseData):
raise TypeError("Can't convert FRD to StateSpace system.")
Expand Down
20 changes: 17 additions & 3 deletions control/tests/interconnect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ def test_interconnect_implicit():
ki = ct.tf(random.uniform(1, 10), [1, 0])
C = ct.tf2io(kp + ki, inputs='e', outputs='u', name='C')

# same but static C2
C2 = ct.tf(random.uniform(1, 10), 1,
inputs='e', outputs='u', name='C2')

# Block diagram computation
Tss = ct.feedback(P * C, 1)
Tss2 = ct.feedback(P * C2, 1)

# Construct the interconnection explicitly
Tio_exp = ct.interconnect(
Expand All @@ -93,6 +98,15 @@ def test_interconnect_implicit():
np.testing.assert_almost_equal(Tio_sum.C, Tss.C)
np.testing.assert_almost_equal(Tio_sum.D, Tss.D)

# test whether signal names work for static system C2
Tio_sum2 = ct.interconnect(
[C2, P, sumblk], inputs='r', outputs='y')

np.testing.assert_almost_equal(Tio_sum2.A, Tss2.A)
np.testing.assert_almost_equal(Tio_sum2.B, Tss2.B)
np.testing.assert_almost_equal(Tio_sum2.C, Tss2.C)
np.testing.assert_almost_equal(Tio_sum2.D, Tss2.D)

# Setting connections to False should lead to an empty connection map
empty = ct.interconnect(
(C, P, sumblk), connections=False, inplist=['r'], outlist=['y'])
Expand Down Expand Up @@ -237,17 +251,17 @@ def test_linear_interconnect():
ss_ctrl = ct.ss(1, 2, 1, 2, inputs='e', outputs='u')
ss_plant = ct.ss(1, 2, 1, 2, inputs='u', outputs='y')
nl_ctrl = ct.NonlinearIOSystem(
lambda t, x, u, params: x*x,
lambda t, x, u, params: x*x,
lambda t, x, u, params: u*x, states=1, inputs='e', outputs='u')
nl_plant = ct.NonlinearIOSystem(
lambda t, x, u, params: x*x,
lambda t, x, u, params: x*x,
lambda t, x, u, params: u*x, states=1, inputs='u', outputs='y')

assert isinstance(ct.interconnect((tf_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((ss_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((tf_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert isinstance(ct.interconnect((ss_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)

assert ~isinstance(ct.interconnect((nl_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert ~isinstance(ct.interconnect((nl_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
assert ~isinstance(ct.interconnect((ss_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
Expand Down
47 changes: 41 additions & 6 deletions control/tests/namedio_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ def test_named_ss():
(lambda t, x, u, params: -x, None),
{'inputs': 2, 'outputs':2, 'states':2}],
[ct.ss, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}],
[ct.ss, ([], [], [], 3), {}], # static system
[ct.StateSpace, ([[1, 2], [3, 4]], [[0], [1]], [[1, 0]], 0), {}],
[ct.tf, ([1, 2], [3, 4, 5]), {}],
[ct.tf, (2, 3), {}], # static system
[ct.TransferFunction, ([1, 2], [3, 4, 5]), {}],
])
def test_io_naming(fun, args, kwargs):
Expand All @@ -112,7 +114,7 @@ def test_io_naming(fun, args, kwargs):
assert sys_g.name == 'sys[0]'
assert sys_g.input_labels == [f'u[{i}]' for i in range(sys_g.ninputs)]
assert sys_g.output_labels == [f'y[{i}]' for i in range(sys_g.noutputs)]
if sys_g.nstates:
if sys_g.nstates is not None:
assert sys_g.state_labels == [f'x[{i}]' for i in range(sys_g.nstates)]

#
Expand All @@ -128,7 +130,7 @@ def test_io_naming(fun, args, kwargs):
sys_r.set_outputs(output_labels)
assert sys_r.output_labels == output_labels

if sys_g.nstates:
if sys_g.nstates is not None:
state_labels = [f'x{i}' for i in range(sys_g.nstates)]
sys_r.set_states(state_labels)
assert sys_r.state_labels == state_labels
Expand All @@ -143,7 +145,7 @@ def test_io_naming(fun, args, kwargs):
sys_k = fun(state_labels, output_labels, input_labels, name='mysys')

elif sys_g.nstates is None:
# Don't pass state labels
# Don't pass state labels if TransferFunction
sys_k = fun(
*args, inputs=input_labels, outputs=output_labels, name='mysys')

Expand All @@ -155,7 +157,7 @@ def test_io_naming(fun, args, kwargs):
assert sys_k.name == 'mysys'
assert sys_k.input_labels == input_labels
assert sys_k.output_labels == output_labels
if sys_g.nstates:
if sys_g.nstates is not None:
assert sys_k.state_labels == state_labels

#
Expand Down Expand Up @@ -193,6 +195,24 @@ def test_io_naming(fun, args, kwargs):
assert sys_tf.input_labels == input_labels
assert sys_tf.output_labels == output_labels

#
# Convert the system to a LinearIOSystem and make sure labels transfer
#
if not isinstance(
sys_r, (ct.FrequencyResponseData, ct.NonlinearIOSystem)) and \
ct.slycot_check():
sys_lio = ct.LinearIOSystem(sys_r)
assert sys_lio != sys_r
assert sys_lio.input_labels == input_labels
assert sys_lio.output_labels == output_labels

# Reassign system and signal names
sys_lio = ct.LinearIOSystem(
sys_g, inputs=input_labels, outputs=output_labels, name='new')
assert sys_lio.name == 'new'
assert sys_lio.input_labels == input_labels
assert sys_lio.output_labels == output_labels


# Internal testing of StateSpace initialization
def test_init_namedif():
Expand Down Expand Up @@ -221,14 +241,29 @@ def test_init_namedif():

# Test state space conversion
def test_convert_to_statespace():
# Set up the initial system
sys = ct.tf(ct.rss(2, 1, 1))
# Set up the initial systems
sys = ct.tf(ct.rss(2, 1, 1), inputs='u', outputs='y', name='sys')
sys_static = ct.tf(1, 2, inputs='u', outputs='y', name='sys_static')

# check that name, inputs, and outputs passed through
sys_new = ct.ss(sys)
assert sys_new.name == 'sys'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']
sys_new = ct.ss(sys_static)
assert sys_new.name == 'sys_static'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']

# Make sure we can rename system name, inputs, outputs
sys_new = ct.ss(sys, inputs='u', outputs='y', name='new')
assert sys_new.name == 'new'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']
sys_new = ct.ss(sys_static, inputs='u', outputs='y', name='new')
assert sys_new.name == 'new'
assert sys_new.input_labels == ['u']
assert sys_new.output_labels == ['y']

# Try specifying the state names (via low level test)
with pytest.warns(UserWarning, match="non-unique state space realization"):
Expand Down