6060from scipy .signal import StateSpace as signalStateSpace
6161from warnings import warn
6262
63- from .exception import ControlSlycot
63+ from .exception import ControlSlycot , slycot_check , ControlMIMONotImplemented
6464from .frdata import FrequencyResponseData
6565from .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
6868from .nlsys import NonlinearIOSystem , InterconnectedSystem
6969from . 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 (
0 commit comments