Skip to content
Open
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
10 changes: 7 additions & 3 deletions control/nlsys.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,10 +951,14 @@ def _compute_static_io(self, t, x, u):
ylist = np.zeros((noutputs + ninputs,))

# To allow for feedthrough terms, iterate multiple times to allow
# feedthrough elements to propagate. For n systems, we could need to
# cycle through n+1 times before reaching steady state
# feedthrough elements to propagate. For an interconnected system,
# an acyclic feedthrough path can traverse through multiple channels
# of the same subsystem in MIMO blocks. The maximum number of feedthrough
# propagation steps before reaching steady state (or confirming convergence)
# is therefore bounded by the number of internal inputs ninputs (or
# the number of subsystems).
# TODO (later): see if there is a more efficient way to compute
cycle_count = len(self.syslist) + 1
cycle_count = max(len(self.syslist), ninputs) + 1
while cycle_count > 0:
state_index, input_index, output_index = 0, 0, 0
for sys in self.syslist:
Expand Down
49 changes: 49 additions & 0 deletions control/tests/interconnect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,3 +746,52 @@ def test_parallel_interconnect():
np.testing.assert_allclose(sys_bd.B, sys_ic.B)
np.testing.assert_allclose(sys_bd.C, sys_ic.C)
np.testing.assert_allclose(sys_bd.D, sys_ic.D)


# Bug identified in issue #1257: MIMO feedthrough path visiting a subsystem multiple times
def test_interconnect_mimo_feedthrough_no_algebraic_loop():
# 2-input, 2-output plant with direct feedthrough only in g11
A = np.array([[-2.0, 0.0], [0.0, -3.0]])
B = np.array([[1.0, 0.0], [0.0, 1.0]])
C = np.array([[1.0, 0.0], [0.0, 1.0]])
D = np.array([[1.0, 0.0], [0.0, 0.0]])
plant = ct.ss(
A, B, C, D, inputs=['u[0]', 'u[1]'], outputs=['y[0]', 'y[1]'],
name='plant')

# PI controllers with D = 1
A_pi = np.array([[0.0]])
B_pi = np.array([[1.0]])
C_pi = np.array([[1.0]])
D_pi = np.array([[1.0]])
pi1 = ct.ss(A_pi, B_pi, C_pi, D_pi, inputs='e[0]', outputs='u[1]', name='pi1')
pi2 = ct.ss(A_pi, B_pi, C_pi, D_pi, inputs='e[1]', outputs='u[0]', name='pi2')

err = ct.summing_junction(
inputs=['r', '-y'], outputs='e', dimension=2, name='err')

# Without the fix, this raises RuntimeError: algebraic loop detected
sys = ct.interconnect(
[err, pi1, pi2, plant], inplist=['r'], outlist=['y'])

# Compare closed loop poles against ct.feedback
K_ss = ct.ss(
np.zeros((2, 2)),
np.eye(2),
np.array([[0.0, 1.0], [1.0, 0.0]]),
np.array([[0.0, 1.0], [1.0, 0.0]])
)
expected_poles = np.sort_complex(ct.feedback(plant, K_ss).poles())
np.testing.assert_allclose(
np.sort_complex(sys.poles()), expected_poles, rtol=1e-5)


def test_interconnect_algebraic_loop_detection():
# True algebraic loop: feedback through static feedthrough blocks
P = ct.ss([], [], [], [[1.0]], inputs='u', outputs='y', name='P')
sumj = ct.summing_junction(inputs=['r', 'y'], outputs='u', name='sumj')
with pytest.raises(RuntimeError, match='algebraic loop detected'):
ct.interconnect(
[sumj, P], connections=[['P.u', 'sumj.u'], ['sumj.y', 'P.y']],
inplist=['r'], outlist=['P.y'])

Loading