Skip to content

Commit 2ddc9ea

Browse files
authored
Merge pull request #937 from murrayrm/tf2ss_unstable_bug-21Oct2023
2 parents a44def1 + 31793e6 commit 2ddc9ea

File tree

3 files changed

+85
-12
lines changed

3 files changed

+85
-12
lines changed

control/statesp.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@
6060
from scipy.signal import StateSpace as signalStateSpace
6161
from warnings import warn
6262

63-
from .exception import ControlSlycot
63+
from .exception import ControlSlycot, slycot_check, ControlMIMONotImplemented
6464
from .frdata import FrequencyResponseData
6565
from .lti import LTI, _process_frequency_response
66-
from .iosys import InputOutputSystem, common_timebase, isdtime, \
66+
from .iosys import InputOutputSystem, common_timebase, isdtime, issiso, \
6767
_process_iosys_keywords, _process_dt_keyword, _process_signal_list
6868
from .nlsys import NonlinearIOSystem, InterconnectedSystem
6969
from . import config
@@ -1583,6 +1583,13 @@ def ss(*args, **kwargs):
15831583
--------
15841584
tf, ss2tf, tf2ss
15851585
1586+
Notes
1587+
-----
1588+
If a transfer function is passed as the sole positional argument, the
1589+
system will be converted to state space form in the same way as calling
1590+
:func:`~control.tf2ss`. The `method` keyword can be used to select the
1591+
method for conversion.
1592+
15861593
Examples
15871594
--------
15881595
Create a Linear I/O system object from matrices.
@@ -1615,10 +1622,13 @@ def ss(*args, **kwargs):
16151622
warn("state labels specified for "
16161623
"non-unique state space realization")
16171624

1625+
# Allow method to be specified (eg, tf2ss)
1626+
method = kwargs.pop('method', None)
1627+
16181628
# Create a state space system from an LTI system
16191629
sys = StateSpace(
16201630
_convert_to_statespace(
1621-
sys,
1631+
sys, method=method,
16221632
use_prefix_suffix=not sys._generic_name_check()),
16231633
**kwargs)
16241634

@@ -1765,6 +1775,10 @@ def tf2ss(*args, **kwargs):
17651775
name : string, optional
17661776
System name. If unspecified, a generic name <sys[id]> is generated
17671777
with a unique integer id.
1778+
method : str, optional
1779+
Set the method used for computing the result. Current methods are
1780+
'slycot' and 'scipy'. If set to None (default), try 'slycot' first
1781+
and then 'scipy' (SISO only).
17681782
17691783
Raises
17701784
------
@@ -1781,6 +1795,13 @@ def tf2ss(*args, **kwargs):
17811795
tf
17821796
ss2tf
17831797
1798+
Notes
1799+
-----
1800+
The ``slycot`` routine used to convert a transfer function into state
1801+
space form appears to have a bug and in some (rare) instances may not
1802+
return a system with the same poles as the input transfer function.
1803+
For SISO systems, setting ``method=scipy`` can be used as an alternative.
1804+
17841805
Examples
17851806
--------
17861807
>>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
@@ -2189,7 +2210,7 @@ def _f2s(f):
21892210
return s
21902211

21912212

2192-
def _convert_to_statespace(sys, use_prefix_suffix=False):
2213+
def _convert_to_statespace(sys, use_prefix_suffix=False, method=None):
21932214
"""Convert a system to state space form (if needed).
21942215
21952216
If sys is already a state space, then it is returned. If sys is a
@@ -2213,13 +2234,17 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22132234
raise ValueError("transfer function is non-proper; can't "
22142235
"convert to StateSpace system")
22152236

2216-
try:
2237+
if method is None and slycot_check() or method == 'slycot':
2238+
if not slycot_check():
2239+
raise ValueError("method='slycot' requires slycot")
2240+
22172241
from slycot import td04ad
22182242

22192243
# Change the numerator and denominator arrays so that the transfer
22202244
# function matrix has a common denominator.
22212245
# matrices are also sized/padded to fit td04ad
22222246
num, den, denorder = sys.minreal()._common_den()
2247+
num, den, denorder = sys._common_den()
22232248

22242249
# transfer function to state space conversion now should work!
22252250
ssout = td04ad('C', sys.ninputs, sys.noutputs,
@@ -2230,9 +2255,8 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22302255
ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
22312256
ssout[3][:sys.noutputs, :states], ssout[4], sys.dt)
22322257

2233-
except ImportError:
2234-
# No Slycot. Scipy tf->ss can't handle MIMO, but static
2235-
# MIMO is an easy special case we can check for here
2258+
elif method in [None, 'scipy']:
2259+
# Scipy tf->ss can't handle MIMO, but SISO is OK
22362260
maxn = max(max(len(n) for n in nrow)
22372261
for nrow in sys.num)
22382262
maxd = max(max(len(d) for d in drow)
@@ -2244,12 +2268,15 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22442268
D[i, j] = sys.num[i][j][0] / sys.den[i][j][0]
22452269
newsys = StateSpace([], [], [], D, sys.dt)
22462270
else:
2247-
if sys.ninputs != 1 or sys.noutputs != 1:
2248-
raise TypeError("No support for MIMO without slycot")
2271+
if not issiso(sys):
2272+
raise ControlMIMONotImplemented(
2273+
"MIMO system conversion not supported without Slycot")
22492274

22502275
A, B, C, D = \
22512276
sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
22522277
newsys = StateSpace(A, B, C, D, sys.dt)
2278+
else:
2279+
raise ValueError(f"unknown {method=}")
22532280

22542281
# Copy over the signal (and system) names
22552282
newsys._copy_names(

control/tests/convert_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from control import rss, ss, ss2tf, tf, tf2ss
2222
from control.statefbk import ctrb, obsv
2323
from control.freqplot import bode
24-
from control.exception import slycot_check
24+
from control.exception import slycot_check, ControlMIMONotImplemented
2525
from control.tests.conftest import slycotonly
2626

2727

@@ -167,7 +167,7 @@ def testConvertMIMO(self):
167167

168168
# Convert to state space and look for an error
169169
if (not slycot_check()):
170-
with pytest.raises(TypeError):
170+
with pytest.raises(ControlMIMONotImplemented):
171171
tf2ss(tsys)
172172
else:
173173
ssys = tf2ss(tsys)

control/tests/statesp_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,3 +1203,49 @@ def test_params_warning():
12031203
sys.output(0, [0], [0], {'k': 5})
12041204

12051205

1206+
# Check that tf2ss returns stable system (see issue #935)
1207+
@pytest.mark.parametrize("method", [
1208+
# pytest.param(None), # use this one when SLICOT bug is sorted out
1209+
pytest.param( # remove this one when SLICOT bug is sorted out
1210+
None, marks=pytest.mark.xfail(
1211+
ct.slycot_check(), reason="tf2ss SLICOT bug")),
1212+
pytest.param(
1213+
'slycot', marks=[
1214+
pytest.mark.xfail(
1215+
not ct.slycot_check(), reason="slycot not installed"),
1216+
pytest.mark.xfail( # remove this one when SLICOT bug is sorted out
1217+
ct.slycot_check(), reason="tf2ss SLICOT bug")]),
1218+
pytest.param('scipy')
1219+
])
1220+
def test_tf2ss_unstable(method):
1221+
num = np.array([
1222+
9.94004350e-13, 2.67602795e-11, 2.31058712e-10, 1.15119493e-09,
1223+
5.04635153e-09, 1.34066064e-08, 2.11938725e-08, 2.39940325e-08,
1224+
2.05897777e-08, 1.17092854e-08, 4.71236875e-09, 1.19497537e-09,
1225+
1.90815347e-10, 1.00655454e-11, 1.47388887e-13, 8.40314881e-16,
1226+
1.67195685e-18])
1227+
den = np.array([
1228+
9.43513863e-11, 6.05312352e-08, 7.92752628e-07, 5.23764693e-06,
1229+
1.82502556e-05, 1.24355899e-05, 8.68206174e-06, 2.73818482e-06,
1230+
4.29133144e-07, 3.85554417e-08, 1.62631575e-09, 8.41098151e-12,
1231+
9.85278302e-15, 4.07646645e-18, 5.55496497e-22, 3.06560494e-26,
1232+
5.98908988e-31])
1233+
1234+
tf_sys = ct.tf(num, den)
1235+
ss_sys = ct.tf2ss(tf_sys, method=method)
1236+
1237+
tf_poles = np.sort(tf_sys.poles())
1238+
ss_poles = np.sort(ss_sys.poles())
1239+
np.testing.assert_allclose(tf_poles, ss_poles, rtol=1e-4)
1240+
1241+
1242+
def test_tf2ss_mimo():
1243+
sys_tf = ct.tf([[[1], [1, 1, 1]]], [[[1, 1, 1], [1, 2, 1]]])
1244+
1245+
if ct.slycot_check():
1246+
sys_ss = ct.ss(sys_tf)
1247+
np.testing.assert_allclose(
1248+
np.sort(sys_tf.poles()), np.sort(sys_ss.poles()))
1249+
else:
1250+
with pytest.raises(ct.ControlMIMONotImplemented):
1251+
sys_ss = ct.ss(sys_tf)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy