diff --git a/control/statesp.py b/control/statesp.py index 41f92ae21..99c5fe9dc 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -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 @@ -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 @@ -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") @@ -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.") diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 2c29aeaca..3d2f0c7d7 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -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( @@ -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']) @@ -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) diff --git a/control/tests/namedio_test.py b/control/tests/namedio_test.py index 4925e9790..3203214d6 100644 --- a/control/tests/namedio_test.py +++ b/control/tests/namedio_test.py @@ -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): @@ -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)] # @@ -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 @@ -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') @@ -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 # @@ -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(): @@ -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"):