Skip to content

Commit fe8888f

Browse files
committed
add unit test illustrating issue #935 + add keyword for tf2ss
1 parent a44def1 commit fe8888f

File tree

2 files changed

+49
-7
lines changed

2 files changed

+49
-7
lines changed

control/statesp.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
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
6464
from .frdata import FrequencyResponseData
6565
from .lti import LTI, _process_frequency_response
6666
from .iosys import InputOutputSystem, common_timebase, isdtime, \
@@ -1615,10 +1615,13 @@ def ss(*args, **kwargs):
16151615
warn("state labels specified for "
16161616
"non-unique state space realization")
16171617

1618+
# Allow method to be specified (eg, tf2ss)
1619+
method = kwargs.pop('method', None)
1620+
16181621
# Create a state space system from an LTI system
16191622
sys = StateSpace(
16201623
_convert_to_statespace(
1621-
sys,
1624+
sys, method=method,
16221625
use_prefix_suffix=not sys._generic_name_check()),
16231626
**kwargs)
16241627

@@ -2189,7 +2192,7 @@ def _f2s(f):
21892192
return s
21902193

21912194

2192-
def _convert_to_statespace(sys, use_prefix_suffix=False):
2195+
def _convert_to_statespace(sys, use_prefix_suffix=False, method=None):
21932196
"""Convert a system to state space form (if needed).
21942197
21952198
If sys is already a state space, then it is returned. If sys is a
@@ -2213,13 +2216,17 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22132216
raise ValueError("transfer function is non-proper; can't "
22142217
"convert to StateSpace system")
22152218

2216-
try:
2219+
if method is None and slycot_check() or method == 'slycot':
2220+
if not slycot_check():
2221+
raise ValueError("method='slycot' requires slycot")
2222+
22172223
from slycot import td04ad
22182224

22192225
# Change the numerator and denominator arrays so that the transfer
22202226
# function matrix has a common denominator.
22212227
# matrices are also sized/padded to fit td04ad
22222228
num, den, denorder = sys.minreal()._common_den()
2229+
num, den, denorder = sys._common_den()
22232230

22242231
# transfer function to state space conversion now should work!
22252232
ssout = td04ad('C', sys.ninputs, sys.noutputs,
@@ -2230,9 +2237,8 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22302237
ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
22312238
ssout[3][:sys.noutputs, :states], ssout[4], sys.dt)
22322239

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
2240+
elif method in [None, 'scipy']:
2241+
# Scipy tf->ss can't handle MIMO, but SISO is OK
22362242
maxn = max(max(len(n) for n in nrow)
22372243
for nrow in sys.num)
22382244
maxd = max(max(len(d) for d in drow)
@@ -2250,6 +2256,8 @@ def _convert_to_statespace(sys, use_prefix_suffix=False):
22502256
A, B, C, D = \
22512257
sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
22522258
newsys = StateSpace(A, B, C, D, sys.dt)
2259+
else:
2260+
raise ValueError(f"unknown {method=}")
22532261

22542262
# Copy over the signal (and system) names
22552263
newsys._copy_names(

control/tests/statesp_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,3 +1203,37 @@ 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)

0 commit comments

Comments
 (0)