Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
23dc966
propagate and allow overriding of signal labels in StateSpace lft fun…
tomaantjes Sep 6, 2026
e6a3e1b
add a check for validity of given nu and ny variables to statespace l…
tomaantjes Sep 6, 2026
37153cf
update Statespace lft function docstring to directly reflect its func…
tomaantjes Sep 9, 2026
6007d4d
add general lft function that can be used with any InputOutputSystem
tomaantjes Sep 12, 2026
47205b1
add lft function to kwarg tests
tomaantjes Sep 12, 2026
6776e2d
add unittests for lft function with LTI systems
tomaantjes Sep 12, 2026
bb06493
add unittest for lft function with general InputOutputSystem input
tomaantjes Sep 12, 2026
d74973d
add lft to function reference
tomaantjes Sep 12, 2026
1694287
add narrative documentation for lft function
tomaantjes Sep 12, 2026
3c9752f
make lft be able to use scalar, array and tf inputs
tomaantjes Sep 13, 2026
351ba99
add lft to see also of other bdalg functions
tomaantjes Sep 13, 2026
b72c3e0
reorder checks to be more logical in lft function
tomaantjes Sep 13, 2026
3262a35
compute maximal values before dimension check in ss.lft
tomaantjes Sep 13, 2026
2f65871
reorder bdalg lft tests
tomaantjes Sep 13, 2026
4cc8e24
add examples to lft functions
tomaantjes Sep 13, 2026
842a10c
fallback to default names if duplicate input or output names occur af…
tomaantjes Sep 13, 2026
7522036
make definition of ny and nu more general in lft docstrings
tomaantjes Sep 13, 2026
66d1f20
align FRD and non-LTI system input error test for lft function
tomaantjes Sep 13, 2026
d2264c6
remove unnecessary blank line
tomaantjes Sep 14, 2026
5a59926
mark lft test that requires slycot
tomaantjes Sep 15, 2026
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
181 changes: 174 additions & 7 deletions control/bdalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
from . import statesp as ss
from . import xferfcn as tf
from .iosys import InputOutputSystem
from .nlsys import interconnect

__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect',
'combine_tf', 'split_tf']
'lft', 'combine_tf', 'split_tf']


def series(*sys, **kwargs):
Expand Down Expand Up @@ -65,7 +66,7 @@ def series(*sys, **kwargs):

See Also
--------
append, feedback, interconnect, negate, parallel
append, feedback, interconnect, lft, negate, parallel

Notes
-----
Expand Down Expand Up @@ -138,7 +139,7 @@ def parallel(*sys, **kwargs):

See Also
--------
append, feedback, interconnect, negate, series
append, feedback, interconnect, lft, negate, series

Notes
-----
Expand Down Expand Up @@ -200,7 +201,7 @@ def negate(sys, **kwargs):

See Also
--------
append, feedback, interconnect, parallel, series
append, feedback, interconnect, lft, parallel, series

Notes
-----
Expand Down Expand Up @@ -265,7 +266,7 @@ def feedback(sys1, sys2=1, sign=-1, **kwargs):

See Also
--------
append, interconnect, negate, parallel, series
append, interconnect, lft, negate, parallel, series

Notes
-----
Expand Down Expand Up @@ -313,6 +314,172 @@ def feedback(sys1, sys2=1, sign=-1, **kwargs):
sys.update_names(**kwargs)
return sys

def lft(sys1, sys2, nu=-1, ny=-1, **kwargs):
"""Linear fractional transformation of two I/O systems.

Forms the Redheffer star product of `sys1` and `sys2` [1]_.
This connects the first `nu` outputs of `sys2` to the last `nu`
inputs of `sys1`, and the last `ny` outputs of `sys1` to the
first `ny` inputs of `sys2`. If `sys2` has fewer inputs and
outputs than `sys1`, this forms the lower LFT of `sys1` and
`sys2`. If `sys1` has fewer inputs and outputs than `sys2`,
this forms the upper LFT of `sys2` and `sys1`.

Parameters
----------
sys1, sys2 : scalar, array, or `InputOutputSystem`
I/O systems to perform linear fractional transformation on.
`FrequencyResponseData` systems are not supported.
ny : int, optional
Dimension of the output of `sys1` that is connected to `sys2`.
Must not exceed the number of outputs of `sys1` or the number
of inputs of `sys2`. If not specified, defaults to the
largest value allowed by the shapes of `sys1` and `sys2`.
nu : int, optional
Dimension of the output of `sys2` that is connected to `sys1`.
Must not exceed the number of inputs of `sys1` or the number
of outputs of `sys2`. If not specified, defaults to the
largest value allowed by the shapes of `sys1` and `sys2`.

Returns
-------
out : `InputOutputSystem`
The result of the linear fractional transformation, with
input and output labels inherited from the corresponding
signals of `sys1` and `sys2` unless overridden.

Other Parameters
----------------
inputs, outputs, states : int, list of str, or None, optional
Description of the system inputs, outputs, and states. If
not specified, these are inherited from the corresponding
signals of `sys1` and `sys2`. See `InputOutputSystem` for
more information.
name : string, optional
Set the name of the resulting system.

Raises
------
ValueError
If `ny` exceeds the number of outputs of `sys1` or the
number of inputs of `sys2`, or if `nu` exceeds the number of
inputs of `sys1` or the number of outputs of `sys2`.
TypeError
If `sys1` or `sys2` is not an I/O system, or cannot be
converted to one, or if either is a `FrequencyResponseData`
system.

See Also
--------
append, feedback, interconnect, negate, parallel, series

Notes
-----
This function is a wrapper for `StateSpace.lft`. If `sys1` and
`sys2` are `StateSpace` systems, or can be converted to
`StateSpace` systems, the linear-algebraic implementation in
`StateSpace.lft` is used directly. For other I/O systems,
the same interconnection is built using `interconnect`.

References
----------
.. [1] J. Doyle, A. Packard, and K. Zhou, "Review of LFTs,
LMIs, and mu," Proceedings of the 30th IEEE Conference on
Decision and Control, Brighton, England, 1991, pp. 1227-1232.

Examples
--------
>>> G1 = ct.rss(3, inputs=3, outputs=3)
>>> G2 = ct.rss(3, inputs=3, outputs=3)
>>> G = ct.lft(G1, G2, nu=2, ny=1)
>>> G.ninputs, G.noutputs, G.nstates
(3, 3, 6)

>>> G1 = ct.rss(3, inputs=4, outputs=4)
>>> G2 = ct.rss(2, inputs=2, outputs=2)
>>> G = ct.lft(G1, G2)
>>> G.ninputs, G.noutputs, G.nstates
(2, 2, 5)

"""
# Check for correct input types
if not isinstance(sys1, (int, float, complex, np.number, np.ndarray,
InputOutputSystem)):
raise TypeError("sys1 must be an I/O system, scalar, or array")
elif not isinstance(sys2, (int, float, complex, np.number, np.ndarray,
InputOutputSystem)):
raise TypeError("sys2 must be an I/O system, scalar, or array")

if isinstance(sys1, frd.FrequencyResponseData) or \
isinstance(sys2, frd.FrequencyResponseData):
raise TypeError("FrequencyResponseData systems are not supported")

# Convert systems to statespace if possible
convertible_types = (
int, float, complex, np.number, np.ndarray, tf.TransferFunction,
)
if isinstance(sys1, convertible_types):
sys1 = ss._convert_to_statespace(sys1)
if isinstance(sys2, convertible_types):
sys2 = ss._convert_to_statespace(sys2)

# Maximal values for nu, ny
if ny == -1:
ny = min(sys2.ninputs, sys1.noutputs)
if nu == -1:
nu = min(sys2.noutputs, sys1.ninputs)

# Check that nu, ny are within bounds
if ny > sys1.noutputs or ny > sys2.ninputs:
raise ValueError(
"ny can't exceed the number of outputs of sys1 or "
"inputs of sys2")
if nu > sys1.ninputs or nu > sys2.noutputs:
raise ValueError(
"nu can't exceed the number of inputs of sys1 or "
"outputs of sys2")

# If sys1 and sys2 are StateSpace, use ss.lft function
if isinstance(sys1, ss.StateSpace) and isinstance(sys2, ss.StateSpace):
return sys1.lft(sys2, nu, ny, **kwargs)

# If sys1 and sys2 are not StateSpace, use interconnect
n1i, n1o = sys1.ninputs, sys1.noutputs
n2i, n2o = sys2.ninputs, sys2.noutputs

connections = [
[(0, n1i - nu + i), (1, i)] for i in range(nu)
] + [
[(1, i), (0, n1o - ny + i)] for i in range(ny)
]

inplist = [(0, i) for i in range(n1i - nu)] + \
[(1, i) for i in range(ny, n2i)]

outlist = [(0, i) for i in range(n1o - ny)] + \
[(1, i) for i in range(nu, n2o)]

if not 'inputs' in kwargs:
inputs = sys1.input_labels[:n1i-nu] + sys2.input_labels[ny:]
# If sys1 and sys2 have clashing input labels, fallback to
# default names
if len(set(inputs)) != len(inputs):
inputs = len(inputs)
kwargs['inputs'] = inputs

if not 'outputs' in kwargs:
outputs = sys1.output_labels[:n1o-ny] + sys2.output_labels[nu:]
# If sys1 and sys2 have clashing output labels, fallback to
# default names
if len(set(outputs)) != len(outputs):
outputs = len(outputs)
kwargs['outputs'] = outputs

return interconnect(
[sys1, sys2], connections=connections,
inplist=inplist, outlist=outlist, **kwargs)


def append(*sys, **kwargs):
"""append(sys1, sys2[, ..., sysn])

Expand Down Expand Up @@ -349,7 +516,7 @@ def append(*sys, **kwargs):

See Also
--------
interconnect, feedback, negate, parallel, series
interconnect, feedback, lft, negate, parallel, series

Examples
--------
Expand Down Expand Up @@ -411,7 +578,7 @@ def connect(sys, Q, inputv, outputv):

See Also
--------
append, feedback, interconnect, negate, parallel, series
append, feedback, interconnect, lft, negate, parallel, series

Notes
-----
Expand Down
91 changes: 81 additions & 10 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,40 +1058,96 @@ def feedback(self, other=1, sign=-1):

return StateSpace(A, B, C, D, dt)

def lft(self, other, nu=-1, ny=-1):
def lft(self, other, nu=-1, ny=-1, **kwargs):
"""Return the linear fractional transformation.

A definition of the LFT operator can be found in Appendix A.7,
page 512 in [1]_. An alternative definition can be found here:
https://www.mathworks.com/help/control/ref/lft.html
Forms the Redheffer star product of two LTI systems [1]_. This
connects the first `nu` outputs of `other` to the last `nu`
inputs of `self`, and the last `ny` outputs of `self` to the
first `ny` inputs of `other`. If `other` has fewer inputs and
outputs than `self`, this forms the lower LFT of `self` and
`other`. If `self` has fewer inputs and outputs than `other`,
this forms the upper LFT of `other` and `self`.

Parameters
----------
other : `StateSpace`
The lower LTI system.
ny : int, optional
Dimension of (plant) measurement output.
Dimension of the output of `self` that is connected to
`other`. Must not exceed the number of outputs of `self`
or the number of inputs of `other`. If not specified,
defaults to the largest value allowed by the shapes of
`self` and `other`.
nu : int, optional
Dimension of (plant) control input.
Dimension of the output of `other` that is connected to
`self`. Must not exceed the number of inputs of `self`
or the number of outputs of `other`. If not specified,
defaults to the largest value allowed by the shapes of
`self` and `other`.

Returns
-------
`StateSpace`
The result of the linear fractional transformation, with
input and output labels inherited from the corresponding
signals of `self` and `other` unless overridden.

Other Parameters
----------------
inputs, outputs, states : int, list of str, or None, optional
Description of the system inputs, outputs, and states.
If not specified, these are inherited from the
corresponding signals of `self` and `other`. See
`InputOutputSystem` for more information.
name : string, optional
Set the name of the resulting system.

Raises
------
ValueError
If `ny` exceeds the number of outputs of `self` or the
number of inputs of `other`, or if `nu` exceeds the number
of inputs of `self` or the number of outputs of `other`.

References
----------
.. [1] S. Skogestad, Multivariable Feedback Control. Second
edition, 2005.
.. [1] J. Doyle, A. Packard, and K. Zhou, "Review of LFTs,
LMIs, and mu," Proceedings of the 30th IEEE Conference on
Decision and Control, Brighton, England, 1991, pp. 1227-1232.

Examples
--------
>>> G1 = ct.rss(3, inputs=3, outputs=3)
>>> G2 = ct.rss(3, inputs=3, outputs=3)
>>> G = G1.lft(G2, nu=2, ny=1)
>>> G.ninputs, G.noutputs, G.nstates
(3, 3, 6)

>>> G1 = ct.rss(3, inputs=4, outputs=4)
>>> G2 = ct.rss(2, inputs=2, outputs=2)
>>> G = G1.lft(G2)
>>> G.ninputs, G.noutputs, G.nstates
(2, 2, 5)

"""
other = _convert_to_statespace(other)

# maximal values for nu, ny
if ny == -1:
ny = min(other.ninputs, self.noutputs)
if nu == -1:
nu = min(other.noutputs, self.ninputs)

# dimension check
# TODO
if ny > self.noutputs or ny > other.ninputs:
raise ValueError(
"ny can't exceed the number of outputs of self or "
"inputs of other")
if nu > self.ninputs or nu > other.noutputs:
raise ValueError(
"nu can't exceed the number of inputs of self or "
"outputs of other")

dt = common_timebase(self.dt, other.dt)

Expand Down Expand Up @@ -1160,7 +1216,22 @@ def lft(self, other, nu=-1, ny=-1):
[D11 + D12 @ H21, D12 @ H22],
[Dbar21 @ H11, Dbar22 + Dbar21 @ H12]
])
return StateSpace(Ares, Bres, Cres, Dres, dt)

inputs = self.input_labels[:self.ninputs-nu] + \
other.input_labels[ny:]
outputs = self.output_labels[:self.noutputs-ny] + \
other.output_labels[nu:]

# If self and other have clashing input and output names, fallback
# to default names
if len(set(inputs)) != len(inputs):
inputs = len(inputs)
if len(set(outputs)) != len(outputs):
outputs = len(outputs)

sys = StateSpace(
Ares, Bres, Cres, Dres, dt, inputs=inputs, outputs=outputs)
return StateSpace(sys, **kwargs)

def minreal(self, tol=0.0):
"""Remove unobservable and uncontrollable states.
Expand Down
Loading
Loading