• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-control / python-control / 17209086284

25 Aug 2025 12:39PM UTC coverage: 94.153% (-0.6%) from 94.734%
17209086284

Pull #1148

github

web-flow
Merge e9cf123f6 into abeb0e46a
Pull Request #1148: Add support for continuous delay systems

10435 of 11083 relevant lines covered (94.15%)

8.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.1
control/statesp.py
1
# statesp.py - state space class and related functions
2
#
3
# Initial author: Richard M. Murray
4
# Creation date: 24 May 2009
5
# Pre-2014 revisions: Kevin K. Chen, Dec 2010
6
# Use `git shortlog -n -s statesp.py` for full list of contributors
7

8
"""State space class and related functions.
9

10
This module contains the `StateSpace class`, which is used to
11
represent linear systems in state space.
12

13
"""
14

15
import math
9✔
16
import sys
9✔
17
from collections.abc import Iterable
9✔
18
from warnings import warn
9✔
19

20
import numpy as np
9✔
21
import scipy as sp
9✔
22
import scipy.linalg
9✔
23
from numpy import array  # noqa: F401
9✔
24
from numpy import any, asarray, concatenate, cos, delete, empty, exp, eye, \
9✔
25
    isinf, pad, sin, squeeze, zeros
26
from numpy.linalg import LinAlgError, eigvals, matrix_rank, solve
9✔
27
from numpy.random import rand, randn
9✔
28
from scipy.signal import StateSpace as signalStateSpace
9✔
29
from scipy.signal import cont2discrete
9✔
30

31
import control
9✔
32

33
from . import bdalg, config
9✔
34
from .exception import ControlDimension, ControlMIMONotImplemented, \
9✔
35
    ControlSlycot, slycot_check
36
from .frdata import FrequencyResponseData
9✔
37
from .iosys import InputOutputSystem, NamedSignal, _process_iosys_keywords, \
9✔
38
    _process_signal_list, _process_subsys_index, common_timebase, issiso
39
from .lti import LTI, _process_frequency_response
9✔
40
from .mateqn import _check_shape
9✔
41
from .nlsys import InterconnectedSystem, NonlinearIOSystem
9✔
42

43
try:
9✔
44
    from slycot import ab13dd
9✔
45
except ImportError:
4✔
46
    ab13dd = None
4✔
47

48
__all__ = ['StateSpace', 'LinearICSystem', 'ss2io', 'tf2io', 'tf2ss',
9✔
49
           'ssdata', 'linfnorm', 'ss', 'rss', 'drss', 'summing_junction']
50

51
# Define module default parameter values
52
_statesp_defaults = {
9✔
53
    'statesp.remove_useless_states': False,
54
    'statesp.latex_num_format': '.3g',
55
    'statesp.latex_repr_type': 'partitioned',
56
    'statesp.latex_maxsize': 10,
57
    }
58

59

60
class StateSpace(NonlinearIOSystem, LTI):
9✔
61
    r"""StateSpace(A, B, C, D[, dt])
62

63
    State space representation for LTI input/output systems.
64

65
    The StateSpace class is used to represent state-space realizations of
66
    linear time-invariant (LTI) systems:
67

68
    .. math::
69

70
          dx/dt &= A x + B u \\
71
              y &= C x + D u
72

73
    where :math:`u` is the input, :math:`y` is the output, and
74
    :math:`x` is the state.  State space systems are usually created
75
    with the `ss` factory function.
76

77
    Parameters
78
    ----------
79
    A, B, C, D : array_like
80
        System matrices of the appropriate dimensions.
81
    dt : None, True or float, optional
82
        System timebase. 0 (default) indicates continuous time, True
83
        indicates discrete time with unspecified sampling time, positive
84
        number is discrete time with specified sampling time, None
85
        indicates unspecified timebase (either continuous or discrete time).
86

87
    Attributes
88
    ----------
89
    ninputs, noutputs, nstates : int
90
        Number of input, output and state variables.
91
    shape : tuple
92
        2-tuple of I/O system dimension, (noutputs, ninputs).
93
    input_labels, output_labels, state_labels : list of str
94
        Names for the input, output, and state variables.
95
    name : string, optional
96
        System name.
97

98
    See Also
99
    --------
100
    ss, InputOutputSystem, NonlinearIOSystem
101

102
    Notes
103
    -----
104
    The main data members in the `StateSpace` class are the A, B, C, and D
105
    matrices.  The class also keeps track of the number of states (i.e.,
106
    the size of A).
107

108
    A discrete-time system is created by specifying a nonzero 'timebase', dt
109
    when the system is constructed:
110

111
    * `dt` = 0: continuous-time system (default)
112
    * `dt` > 0: discrete-time system with sampling period `dt`
113
    * `dt` = True: discrete time with unspecified sampling period
114
    * `dt` = None: no timebase specified
115

116
    Systems must have compatible timebases in order to be combined. A
117
    discrete-time system with unspecified sampling time (`dt` = True) can
118
    be combined with a system having a specified sampling time; the result
119
    will be a discrete-time system with the sample time of the other
120
    system. Similarly, a system with timebase None can be combined with a
121
    system having any timebase; the result will have the timebase of the
122
    other system.  The default value of dt can be changed by changing the
123
    value of `config.defaults['control.default_dt']`.
124

125
    A state space system is callable and returns the value of the transfer
126
    function evaluated at a point in the complex plane.  See
127
    `StateSpace.__call__` for a more detailed description.
128

129
    Subsystems corresponding to selected input/output pairs can be
130
    created by indexing the state space system::
131

132
        subsys = sys[output_spec, input_spec]
133

134
    The input and output specifications can be single integers, lists of
135
    integers, or slices.  In addition, the strings representing the names
136
    of the signals can be used and will be replaced with the equivalent
137
    signal offsets.  The subsystem is created by truncating the inputs and
138
    outputs, but leaving the full set of system states.
139

140
    StateSpace instances have support for IPython HTML/LaTeX output, intended
141
    for pretty-printing in Jupyter notebooks.  The HTML/LaTeX output can be
142
    configured using `config.defaults['statesp.latex_num_format']`
143
    and `config.defaults['statesp.latex_repr_type']`.  The
144
    HTML/LaTeX output is tailored for MathJax, as used in Jupyter, and
145
    may look odd when typeset by non-MathJax LaTeX systems.
146

147
    `config.defaults['statesp.latex_num_format']` is a format string
148
    fragment, specifically the part of the format string after '{:'
149
    used to convert floating-point numbers to strings.  By default it
150
    is '.3g'.
151

152
    `config.defaults['statesp.latex_repr_type']` must either be
153
    'partitioned' or 'separate'.  If 'partitioned', the A, B, C, D
154
    matrices are shown as a single, partitioned matrix; if
155
    'separate', the matrices are shown separately.
156

157
    """
158
    def __init__(self, *args, **kwargs):
9✔
159
        """StateSpace(A, B, C, D[, dt])
160

161
        Construct a state space object.
162

163
        The default constructor is StateSpace(A, B, C, D), where A, B, C, D
164
        are matrices or equivalent objects.  To create a discrete-time
165
        system, use StateSpace(A, B, C, D, dt) where `dt` is the sampling
166
        time (or True for unspecified sampling time).  To call the copy
167
        constructor, call ``StateSpace(sys)``, where `sys` is a `StateSpace`
168
        object.
169

170
        See `StateSpace` and `ss` for more information.
171

172
        """
173
        #
174
        # Process positional arguments
175
        #
176

177
        if len(args) == 4:
9✔
178
            # The user provided A, B, C, and D matrices.
179
            A, B, C, D = args
9✔
180

181
        elif len(args) == 5:
9✔
182
            # Discrete time system
183
            A, B, C, D, dt = args
9✔
184
            if 'dt' in kwargs:
9✔
185
                warn("received multiple dt arguments, "
9✔
186
                     "using positional arg dt = %s" % dt)
187
            kwargs['dt'] = dt
9✔
188
            args = args[:-1]
9✔
189

190
        elif len(args) == 1:
9✔
191
            # Use the copy constructor
192
            if not isinstance(args[0], StateSpace):
9✔
193
                raise TypeError(
9✔
194
                    "the one-argument constructor can only take in a "
195
                    "StateSpace object; received %s" % type(args[0]))
196
            A = args[0].A
9✔
197
            B = args[0].B
9✔
198
            C = args[0].C
9✔
199
            D = args[0].D
9✔
200
            if 'dt' not in kwargs:
9✔
201
                kwargs['dt'] = args[0].dt
9✔
202

203
        else:
204
            raise TypeError(
9✔
205
                "Expected 1, 4, or 5 arguments; received %i." % len(args))
206

207
        # Convert all matrices to standard form (sizes checked later)
208
        A = _ssmatrix(A, square=True, name="A")
9✔
209
        B = _ssmatrix(
9✔
210
            B, axis=0 if np.asarray(B).ndim == 1 and len(B) == A.shape[0]
211
            else 1, name="B")
212
        C = _ssmatrix(
9✔
213
            C, axis=1 if np.asarray(C).ndim == 1 and len(C) == A.shape[0]
214
            else 0, name="C")
215
        if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0:
9✔
216
            # If D is a scalar zero, broadcast it to the proper size
217
            D = np.zeros((C.shape[0], B.shape[1]))
9✔
218
        D = _ssmatrix(D, name="D")
9✔
219

220
        # If only direct term is present, adjust sizes of C and D if needed
221
        if D.size > 0 and B.size == 0:
9✔
222
            B = np.zeros((0, D.shape[1]))
9✔
223
        if D.size > 0 and C.size == 0:
9✔
224
            C = np.zeros((D.shape[0], 0))
9✔
225

226
        # Matrices defining the linear system
227
        self.A = A
9✔
228
        self.B = B
9✔
229
        self.C = C
9✔
230
        self.D = D
9✔
231

232
        # Determine if the system is static (memoryless)
233
        static = (A.size == 0)
9✔
234

235
        #
236
        # Process keyword arguments
237
        #
238

239
        remove_useless_states = kwargs.pop(
9✔
240
            'remove_useless_states',
241
            config.defaults['statesp.remove_useless_states'])
242

243
        # Process iosys keywords
244
        defaults = args[0] if len(args) == 1 else \
9✔
245
            {'inputs': B.shape[1], 'outputs': C.shape[0],
246
             'states': A.shape[0]}
247
        name, inputs, outputs, states, dt = _process_iosys_keywords(
9✔
248
            kwargs, defaults, static=static)
249

250
        # Create updfcn and outfcn
251
        updfcn = lambda t, x, u, params: \
9✔
252
            self.A @ np.atleast_1d(x) + self.B @ np.atleast_1d(u)
253
        outfcn = lambda t, x, u, params: \
9✔
254
            self.C @ np.atleast_1d(x) + self.D @ np.atleast_1d(u)
255

256
        # Initialize NonlinearIOSystem object
257
        super().__init__(
9✔
258
            updfcn, outfcn,
259
            name=name, inputs=inputs, outputs=outputs,
260
            states=states, dt=dt, **kwargs)
261

262
        # Reset shapes if the system is static
263
        if static:
9✔
264
            A.shape = (0, 0)
9✔
265
            B.shape = (0, self.ninputs)
9✔
266
            C.shape = (self.noutputs, 0)
9✔
267

268
        # Check to make sure everything is consistent
269
        _check_shape(A, self.nstates, self.nstates, name="A")
9✔
270
        _check_shape(B, self.nstates, self.ninputs, name="B")
9✔
271
        _check_shape(C, self.noutputs, self.nstates, name="C")
9✔
272
        _check_shape(D, self.noutputs, self.ninputs, name="D")
9✔
273

274
        #
275
        # Final processing
276
        #
277
        # Check for states that don't do anything, and remove them
278
        if remove_useless_states:
9✔
279
            self._remove_useless_states()
9✔
280

281
    #
282
    # Class attributes
283
    #
284
    # These attributes are defined as class attributes so that they are
285
    # documented properly.  They are "overwritten" in __init__.
286
    #
287

288
    #: Number of system inputs.
289
    #:
290
    #: :meta hide-value:
291
    ninputs = 0
9✔
292

293
    #: Number of system outputs.
294
    #:
295
    #: :meta hide-value:
296
    noutputs = 0
9✔
297

298
    #: Number of system states.
299
    #:
300
    #: :meta hide-value:
301
    nstates = 0
9✔
302

303
    #: Dynamics matrix.
304
    #:
305
    #: :meta hide-value:
306
    A = []
9✔
307

308
    #: Input matrix.
309
    #:
310
    #: :meta hide-value:
311
    B = []
9✔
312

313
    #: Output matrix.
314
    #:
315
    #: :meta hide-value:
316
    C = []
9✔
317

318
    #: Direct term.
319
    #:
320
    #: :meta hide-value:
321
    D = []
9✔
322

323
    #
324
    # Getter and setter functions for legacy state attributes
325
    #
326
    # For this iteration, generate a deprecation warning whenever the
327
    # getter/setter is called.  For a future iteration, turn it into a
328
    # future warning, so that users will see it.
329
    #
330

331
    def _get_states(self):
9✔
332
        warn("The StateSpace `states` attribute will be deprecated in a "
9✔
333
             "future release.  Use `nstates` instead.",
334
             FutureWarning, stacklevel=2)
335
        return self.nstates
9✔
336

337
    def _set_states(self, value):
9✔
338
        warn("The StateSpace `states` attribute will be deprecated in a "
×
339
             "future release.  Use `nstates` instead.",
340
             FutureWarning, stacklevel=2)
341
        self.nstates = value
×
342

343
    #: Deprecated attribute; use `nstates` instead.
344
    #:
345
    #: The `state` attribute was used to store the number of states for : a
346
    #: state space system.  It is no longer used.  If you need to access the
347
    #: number of states, use `nstates`.
348
    states = property(_get_states, _set_states)
9✔
349

350
    def _remove_useless_states(self):
9✔
351
        """Check for states that don't do anything, and remove them.
352

353
        Scan the A, B, and C matrices for rows or columns of zeros.  If the
354
        zeros are such that a particular state has no effect on the input-
355
        output dynamics, then remove that state from the A, B, and C matrices.
356

357
        """
358

359
        # Search for useless states and get indices of these states.
360
        ax1_A = np.where(~self.A.any(axis=1))[0]
9✔
361
        ax1_B = np.where(~self.B.any(axis=1))[0]
9✔
362
        ax0_A = np.where(~self.A.any(axis=0))[-1]
9✔
363
        ax0_C = np.where(~self.C.any(axis=0))[-1]
9✔
364
        useless_1 = np.intersect1d(ax1_A, ax1_B, assume_unique=True)
9✔
365
        useless_2 = np.intersect1d(ax0_A, ax0_C, assume_unique=True)
9✔
366
        useless = np.union1d(useless_1, useless_2)
9✔
367

368
        # Remove the useless states.
369
        self.A = delete(self.A, useless, 0)
9✔
370
        self.A = delete(self.A, useless, 1)
9✔
371
        self.B = delete(self.B, useless, 0)
9✔
372
        self.C = delete(self.C, useless, 1)
9✔
373

374
        # Remove any state names that we don't need
375
        self.set_states(
9✔
376
            [self.state_labels[i] for i in range(self.nstates)
377
             if i not in useless])
378

379
    def __str__(self):
9✔
380
        """Return string representation of the state space system."""
381
        string = f"{InputOutputSystem.__str__(self)}\n\n"
9✔
382
        string += "\n\n".join([
9✔
383
            "{} = {}".format(Mvar,
384
                               "\n    ".join(str(M).splitlines()))
385
            for Mvar, M in zip(["A", "B", "C", "D"],
386
                               [self.A, self.B, self.C, self.D])])
387
        return string
9✔
388

389
    def _repr_eval_(self):
9✔
390
        # Loadable format
391
        out = "StateSpace(\n{A},\n{B},\n{C},\n{D}".format(
9✔
392
            A=self.A.__repr__(), B=self.B.__repr__(),
393
            C=self.C.__repr__(), D=self.D.__repr__())
394

395
        out += super()._dt_repr(separator=",\n", space="")
9✔
396
        if len(labels := super()._label_repr()) > 0:
9✔
397
            out += ",\n" + labels
9✔
398

399
        out += ")"
9✔
400
        return out
9✔
401

402
    def _repr_html_(self):
9✔
403
        """HTML representation of state-space model.
404

405
        Output is controlled by config options statesp.latex_repr_type,
406
        statesp.latex_num_format, and statesp.latex_maxsize.
407

408
        The output is primarily intended for Jupyter notebooks, which
409
        use MathJax to render the LaTeX, and the results may look odd
410
        when processed by a 'conventional' LaTeX system.
411

412
        Returns
413
        -------
414
        s : str
415
            HTML/LaTeX representation of model, or None if either matrix
416
            dimension is greater than statesp.latex_maxsize.
417

418
        """
419
        syssize = self.nstates + max(self.noutputs, self.ninputs)
9✔
420
        if syssize > config.defaults['statesp.latex_maxsize']:
9✔
421
            return None
9✔
422
        elif config.defaults['statesp.latex_repr_type'] == 'partitioned':
9✔
423
            return super()._repr_info_(html=True) + \
9✔
424
                "\n" + self._latex_partitioned()
425
        elif config.defaults['statesp.latex_repr_type'] == 'separate':
9✔
426
            return super()._repr_info_(html=True) + \
9✔
427
                "\n" + self._latex_separate()
428
        else:
429
            raise ValueError(
×
430
                "Unknown statesp.latex_repr_type '{cfg}'".format(
431
                    cfg=config.defaults['statesp.latex_repr_type']))
432

433
    def _latex_partitioned_stateless(self):
9✔
434
        """`Partitioned` matrix LaTeX representation for stateless systems
435

436
        Model is presented as a matrix, D.  No partition lines are shown.
437

438
        Returns
439
        -------
440
        s : str
441
            LaTeX representation of model.
442

443
        """
444
        # Apply NumPy formatting
445
        with np.printoptions(threshold=sys.maxsize):
9✔
446
            D = eval(repr(self.D))
9✔
447

448
        lines = [
9✔
449
            r'$$',
450
            (r'\left['
451
             + r'\begin{array}'
452
             + r'{' + 'rll' * self.ninputs + '}')
453
            ]
454

455
        for Di in asarray(D):
9✔
456
            lines.append('&'.join(_f2s(Dij) for Dij in Di)
9✔
457
                         + '\\\\')
458

459
        lines.extend([
9✔
460
            r'\end{array}'
461
            r'\right]',
462
            r'$$'])
463

464
        return '\n'.join(lines)
9✔
465

466
    def _latex_partitioned(self):
9✔
467
        """Partitioned matrix LaTeX representation of state-space model
468

469
        Model is presented as a matrix partitioned into A, B, C, and D
470
        parts.
471

472
        Returns
473
        -------
474
        s : str
475
            LaTeX representation of model.
476

477
        """
478
        if self.nstates == 0:
9✔
479
            return self._latex_partitioned_stateless()
9✔
480

481
        # Apply NumPy formatting
482
        with np.printoptions(threshold=sys.maxsize):
9✔
483
            A, B, C, D = (
9✔
484
                eval(repr(getattr(self, M))) for M in ['A', 'B', 'C', 'D'])
485

486
        lines = [
9✔
487
            r'$$',
488
            (r'\left['
489
             + r'\begin{array}'
490
             + r'{' + 'rll' * self.nstates + '|' + 'rll' * self.ninputs + '}')
491
            ]
492

493
        for Ai, Bi in zip(asarray(A), asarray(B)):
9✔
494
            lines.append('&'.join([_f2s(Aij) for Aij in Ai]
9✔
495
                                  + [_f2s(Bij) for Bij in Bi])
496
                         + '\\\\')
497
        lines.append(r'\hline')
9✔
498
        for Ci, Di in zip(asarray(C), asarray(D)):
9✔
499
            lines.append('&'.join([_f2s(Cij) for Cij in Ci]
9✔
500
                                  + [_f2s(Dij) for Dij in Di])
501
                         + '\\\\')
502

503
        lines.extend([
9✔
504
            r'\end{array}'
505
            + r'\right]',
506
            r'$$'])
507

508
        return '\n'.join(lines)
9✔
509

510
    def _latex_separate(self):
9✔
511
        """Separate matrices LaTeX representation of state-space model
512

513
        Model is presented as separate, named, A, B, C, and D matrices.
514

515
        Returns
516
        -------
517
        s : str
518
            LaTeX representation of model.
519

520
        """
521
        lines = [
9✔
522
            r'$$',
523
            r'\begin{array}{ll}',
524
            ]
525

526
        def fmt_matrix(matrix, name):
9✔
527
            matlines = [name
9✔
528
                        + r' = \left[\begin{array}{'
529
                        + 'rll' * matrix.shape[1]
530
                        + '}']
531
            for row in asarray(matrix):
9✔
532
                matlines.append('&'.join(_f2s(entry) for entry in row)
9✔
533
                                + '\\\\')
534
            matlines.extend([
9✔
535
                r'\end{array}'
536
                r'\right]'])
537
            return matlines
9✔
538

539
        if self.nstates > 0:
9✔
540
            lines.extend(fmt_matrix(self.A, 'A'))
9✔
541
            lines.append('&')
9✔
542
            lines.extend(fmt_matrix(self.B, 'B'))
9✔
543
            lines.append('\\\\')
9✔
544

545
            lines.extend(fmt_matrix(self.C, 'C'))
9✔
546
            lines.append('&')
9✔
547
        lines.extend(fmt_matrix(self.D, 'D'))
9✔
548

549
        lines.extend([
9✔
550
            r'\end{array}',
551
            r'$$'])
552

553
        return '\n'.join(lines)
9✔
554

555
    # Negation of a system
556
    def __neg__(self):
9✔
557
        """Negate a state space system."""
558
        return StateSpace(self.A, self.B, -self.C, -self.D, self.dt)
9✔
559

560
    # Addition of two state space systems (parallel interconnection)
561
    def __add__(self, other):
9✔
562
        """Add two LTI systems (parallel connection)."""
563
        from .xferfcn import TransferFunction
9✔
564

565
        # Convert transfer functions to state space
566
        if isinstance(other, TransferFunction):
9✔
567
            # Convert the other argument to state space
568
            other = _convert_to_statespace(other)
9✔
569

570
        # Check for a couple of special cases
571
        if isinstance(other, (int, float, complex, np.number)):
9✔
572
            # Just adding a scalar; put it in the D matrix
573
            A, B, C = self.A, self.B, self.C
9✔
574
            D = self.D + other
9✔
575
            dt = self.dt
9✔
576

577
        elif isinstance(other, np.ndarray):
9✔
578
            other = np.atleast_2d(other)
9✔
579
            # Special case for SISO
580
            if self.issiso():
9✔
581
                self = np.ones_like(other) * self
9✔
582
            if self.ninputs != other.shape[0]:
9✔
583
                raise ValueError("array has incompatible shape")
9✔
584
            A, B, C = self.A, self.B, self.C
9✔
585
            D = self.D + other
9✔
586
            dt = self.dt
9✔
587

588
        elif not isinstance(other, StateSpace):
9✔
589
            return NotImplemented       # let other.__rmul__ handle it
9✔
590

591
        else:
592
            # Promote SISO object to compatible dimension
593
            if self.issiso() and not other.issiso():
9✔
594
                self = np.ones((other.noutputs, other.ninputs)) * self
9✔
595
            elif not self.issiso() and other.issiso():
9✔
596
                other = np.ones((self.noutputs, self.ninputs)) * other
9✔
597

598
            # Check to make sure the dimensions are OK
599
            if ((self.ninputs != other.ninputs) or
9✔
600
                    (self.noutputs != other.noutputs)):
601
                raise ValueError(
9✔
602
                    "can't add systems with incompatible inputs and outputs")
603

604
            dt = common_timebase(self.dt, other.dt)
9✔
605

606
            # Concatenate the various arrays
607
            A = concatenate((
9✔
608
                concatenate((self.A, zeros((self.A.shape[0],
609
                                            other.A.shape[-1]))), axis=1),
610
                concatenate((zeros((other.A.shape[0], self.A.shape[-1])),
611
                             other.A), axis=1)), axis=0)
612
            B = concatenate((self.B, other.B), axis=0)
9✔
613
            C = concatenate((self.C, other.C), axis=1)
9✔
614
            D = self.D + other.D
9✔
615

616
        return StateSpace(A, B, C, D, dt)
9✔
617

618
    # Right addition - just switch the arguments
619
    def __radd__(self, other):
9✔
620
        """Right add two LTI systems (parallel connection)."""
621
        return self + other
9✔
622

623
    # Subtraction of two state space systems (parallel interconnection)
624
    def __sub__(self, other):
9✔
625
        """Subtract two LTI systems."""
626
        return self + (-other)
9✔
627

628
    def __rsub__(self, other):
9✔
629
        """Right subtract two LTI systems."""
630
        return other + (-self)
9✔
631

632
    # Multiplication of two state space systems (series interconnection)
633
    def __mul__(self, other):
9✔
634
        """Multiply two LTI objects (serial connection)."""
635
        from .xferfcn import TransferFunction
9✔
636

637
        # Convert transfer functions to state space
638
        if isinstance(other, TransferFunction):
9✔
639
            # Convert the other argument to state space
640
            other = _convert_to_statespace(other)
9✔
641

642
        # Check for a couple of special cases
643
        if isinstance(other, (int, float, complex, np.number)):
9✔
644
            # Just multiplying by a scalar; change the output
645
            A, C = self.A, self.C
9✔
646
            B = self.B * other
9✔
647
            D = self.D * other
9✔
648
            dt = self.dt
9✔
649

650
        elif isinstance(other, np.ndarray):
9✔
651
            other = np.atleast_2d(other)
9✔
652
            # Special case for SISO
653
            if self.issiso():
9✔
654
                self = bdalg.append(*([self] * other.shape[0]))
9✔
655
            # Dimension check after broadcasting
656
            if self.ninputs != other.shape[0]:
9✔
657
                raise ValueError("array has incompatible shape")
9✔
658
            A, C = self.A, self.C
9✔
659
            B = self.B @ other
9✔
660
            D = self.D @ other
9✔
661
            dt = self.dt
9✔
662

663
        elif not isinstance(other, StateSpace):
9✔
664
            return NotImplemented       # let other.__rmul__ handle it
9✔
665

666
        else:
667
            # Promote SISO object to compatible dimension
668
            if self.issiso() and not other.issiso():
9✔
669
                self = bdalg.append(*([self] * other.noutputs))
5✔
670
            elif not self.issiso() and other.issiso():
9✔
671
                other = bdalg.append(*([other] * self.ninputs))
5✔
672

673
            # Check to make sure the dimensions are OK
674
            if self.ninputs != other.noutputs:
9✔
675
                raise ValueError(
9✔
676
                    "can't multiply systems with incompatible"
677
                    " inputs and outputs")
678
            dt = common_timebase(self.dt, other.dt)
9✔
679

680
            # Concatenate the various arrays
681
            A = concatenate(
9✔
682
                (concatenate((other.A,
683
                              zeros((other.A.shape[0], self.A.shape[1]))),
684
                             axis=1),
685
                 concatenate((self.B @ other.C, self.A), axis=1)),
686
                axis=0)
687
            B = concatenate((other.B, self.B @ other.D), axis=0)
9✔
688
            C = concatenate((self.D @ other.C, self.C), axis=1)
9✔
689
            D = self.D @ other.D
9✔
690

691
        return StateSpace(A, B, C, D, dt)
9✔
692

693
    # Right multiplication of two state space systems (series interconnection)
694
    # Just need to convert LH argument to a state space object
695
    def __rmul__(self, other):
9✔
696
        """Right multiply two LTI objects (serial connection)."""
697
        from .xferfcn import TransferFunction
9✔
698

699
        # Convert transfer functions to state space
700
        if isinstance(other, TransferFunction):
9✔
701
            # Convert the other argument to state space
702
            other = _convert_to_statespace(other)
5✔
703

704
        # Check for a couple of special cases
705
        if isinstance(other, (int, float, complex, np.number)):
9✔
706
            # Just multiplying by a scalar; change the input
707
            B = other * self.B
9✔
708
            D = other * self.D
9✔
709
            return StateSpace(self.A, B, self.C, D, self.dt)
9✔
710

711
        elif isinstance(other, np.ndarray):
9✔
712
            other = np.atleast_2d(other)
9✔
713
            # Special case for SISO transfer function
714
            if self.issiso():
9✔
715
                self = bdalg.append(*([self] * other.shape[1]))
9✔
716
            # Dimension check after broadcasting
717
            if self.noutputs != other.shape[1]:
9✔
718
                raise ValueError("array has incompatible shape")
×
719
            C = other @ self.C
9✔
720
            D = other @ self.D
9✔
721
            return StateSpace(self.A, self.B, C, D, self.dt)
9✔
722

723
        if not isinstance(other, StateSpace):
9✔
724
            return NotImplemented
9✔
725

726
        # Promote SISO object to compatible dimension
727
        if self.issiso() and not other.issiso():
9✔
728
            self = bdalg.append(*([self] * other.ninputs))
5✔
729
        elif not self.issiso() and other.issiso():
9✔
730
            other = bdalg.append(*([other] * self.noutputs))
5✔
731

732
        return other * self
9✔
733

734
    # TODO: general __truediv__ requires descriptor system support
735
    def __truediv__(self, other):
9✔
736
        """Division of state space systems by TFs, FRDs, scalars, and arrays"""
737
        # Let ``other.__rtruediv__`` handle it
738
        try:
9✔
739
            return self * (1 / other)
9✔
740
        except ValueError:
9✔
741
            return NotImplemented
9✔
742

743
    def __rtruediv__(self, other):
9✔
744
        """Division by state space system"""
745
        return other * self**-1
9✔
746

747
    def __pow__(self, other):
9✔
748
        """Power of a state space system"""
749
        if not type(other) == int:
9✔
750
            raise ValueError("Exponent must be an integer")
×
751
        if self.ninputs != self.noutputs:
9✔
752
            # System must have same number of inputs and outputs
753
            return NotImplemented
×
754
        if other < -1:
9✔
755
            return (self**-1)**(-other)
5✔
756
        elif other == -1:
9✔
757
            try:
9✔
758
                Di = scipy.linalg.inv(self.D)
9✔
759
            except scipy.linalg.LinAlgError:
9✔
760
                # D matrix must be nonsingular
761
                return NotImplemented
9✔
762
            Ai = self.A - self.B @ Di @ self.C
5✔
763
            Bi = self.B @ Di
5✔
764
            Ci = -Di @ self.C
5✔
765
            return StateSpace(Ai, Bi, Ci, Di, self.dt)
5✔
766
        elif other == 0:
5✔
767
            return StateSpace([], [], [], np.eye(self.ninputs), self.dt)
5✔
768
        elif other == 1:
5✔
769
            return self
5✔
770
        elif other > 1:
5✔
771
            return self * (self**(other - 1))
5✔
772

773
    def __call__(self, x, squeeze=None, warn_infinite=True):
9✔
774
        """Evaluate system transfer function at point in complex plane.
775

776
        Returns the value of the system's transfer function at a point `x`
777
        in the complex plane, where `x` is `s` for continuous-time systems
778
        and `z` for discrete-time systems.
779

780
        See `LTI.__call__` for details.
781

782
        Examples
783
        --------
784
        >>> G = ct.ss([[-1, -2], [3, -4]], [[5], [7]], [[6, 8]], [[9]])
785
        >>> fresp = G(1j)  # evaluate at s = 1j
786

787
        """
788
        # Use Slycot if available
789
        out = self.horner(x, warn_infinite=warn_infinite)
9✔
790
        return _process_frequency_response(self, x, out, squeeze=squeeze)
9✔
791

792
    def slycot_laub(self, x):
9✔
793
        """Laub's method to evaluate response at complex frequency.
794

795
        Evaluate transfer function at complex frequency using Laub's
796
        method from Slycot.  Expects inputs and outputs to be
797
        formatted correctly. Use ``sys(x)`` for a more user-friendly
798
        interface.
799

800
        Parameters
801
        ----------
802
        x : complex array_like or complex
803
            Complex frequency.
804

805
        Returns
806
        -------
807
        output : (number_outputs, number_inputs, len(x)) complex ndarray
808
            Frequency response.
809

810
        """
811
        from slycot import tb05ad
9✔
812

813
        # Make sure the argument is a 1D array of complex numbers
814
        x_arr = np.atleast_1d(x).astype(complex, copy=False)
5✔
815

816
        # Make sure that we are operating on a simple list
817
        if len(x_arr.shape) > 1:
5✔
818
            raise ValueError("input list must be 1D")
5✔
819

820
        # preallocate
821
        n = self.nstates
5✔
822
        m = self.ninputs
5✔
823
        p = self.noutputs
5✔
824
        out = np.empty((p, m, len(x_arr)), dtype=complex)
5✔
825
        # The first call both evaluates C(sI-A)^-1 B and also returns
826
        # Hessenberg transformed matrices at, bt, ct.
827
        result = tb05ad(n, m, p, x_arr[0], self.A, self.B, self.C, job='NG')
5✔
828
        # When job='NG', result = (at, bt, ct, g_i, hinvb, info)
829
        at = result[0]
5✔
830
        bt = result[1]
5✔
831
        ct = result[2]
5✔
832

833
        # TB05AD frequency evaluation does not include direct feedthrough.
834
        out[:, :, 0] = result[3] + self.D
5✔
835

836
        # Now, iterate through the remaining frequencies using the
837
        # transformed state matrices, at, bt, ct.
838

839
        # Start at the second frequency, already have the first.
840
        for kk, x_kk in enumerate(x_arr[1:]):
5✔
841
            result = tb05ad(n, m, p, x_kk, at, bt, ct, job='NH')
5✔
842
            # When job='NH', result = (g_i, hinvb, info)
843

844
            # kk+1 because enumerate starts at kk = 0.
845
            # but zero-th spot is already filled.
846
            out[:, :, kk+1] = result[0] + self.D
5✔
847
        return out
5✔
848

849
    def horner(self, x, warn_infinite=True):
9✔
850
        """Evaluate value of transfer function using Horner's method.
851

852
        Evaluates ``sys(x)`` where `x` is a complex number `s` for
853
        continuous-time systems and `z` for discrete-time systems.  Expects
854
        inputs and outputs to be formatted correctly. Use ``sys(x)`` for a
855
        more user-friendly interface.
856

857
        Parameters
858
        ----------
859
        x : complex
860
            Complex frequency at which the transfer function is evaluated.
861

862
        warn_infinite : bool, optional
863
            If True (default), generate a warning if `x` is a pole.
864

865
        Returns
866
        -------
867
        complex
868

869
        Notes
870
        -----
871
        Attempts to use Laub's method from Slycot library, with a fall-back
872
        to Python code.
873

874
        """
875
        # Make sure the argument is a 1D array of complex numbers
876
        x_arr = np.atleast_1d(x).astype(complex, copy=False)
9✔
877

878
        # return fast on systems with 0 or 1 state
879
        if self.nstates == 0:
9✔
880
            return self.D[:, :, np.newaxis] \
9✔
881
                * np.ones_like(x_arr, dtype=complex)
882
        elif self.nstates == 1:
9✔
883
            with np.errstate(divide='ignore', invalid='ignore'):
9✔
884
                out = self.C[:, :, np.newaxis] \
9✔
885
                    / (x_arr - self.A[0, 0]) \
886
                    * self.B[:, :, np.newaxis] \
887
                    + self.D[:, :, np.newaxis]
888
            out[np.isnan(out)] = complex(np.inf, np.nan)
9✔
889
            return out
9✔
890

891
        try:
9✔
892
            out = self.slycot_laub(x_arr)
9✔
893
        except (ImportError, Exception):
9✔
894
            # Fall back because either Slycot unavailable or cannot handle
895
            # certain cases.
896

897
            # Make sure that we are operating on a simple list
898
            if len(x_arr.shape) > 1:
9✔
899
                raise ValueError("input list must be 1D")
9✔
900

901
            # Preallocate
902
            out = empty((self.noutputs, self.ninputs, len(x_arr)),
9✔
903
                        dtype=complex)
904

905
            # TODO: can this be vectorized?
906
            for idx, x_idx in enumerate(x_arr):
9✔
907
                try:
9✔
908
                    xr = solve(x_idx * eye(self.nstates) - self.A, self.B)
9✔
909
                    out[:, :, idx] = self.C @ xr + self.D
9✔
910
                except LinAlgError:
9✔
911
                    # Issue a warning message, for consistency with xferfcn
912
                    if warn_infinite:
9✔
913
                        warn("singular matrix in frequency response",
9✔
914
                             RuntimeWarning)
915

916
                    # Evaluating at a pole.  Return value depends if there
917
                    # is a zero at the same point or not.
918
                    if x_idx in self.zeros():
9✔
919
                        out[:, :, idx] = complex(np.nan, np.nan)
9✔
920
                    else:
921
                        out[:, :, idx] = complex(np.inf, np.nan)
9✔
922

923
        return out
9✔
924

925
    def freqresp(self, omega):
9✔
926
        """(deprecated) Evaluate transfer function at complex frequencies.
927

928
        .. deprecated::0.9.0
929
            Method has been given the more Pythonic name
930
            `StateSpace.frequency_response`. Or use
931
            `freqresp` in the MATLAB compatibility module.
932
        """
933
        warn("StateSpace.freqresp(omega) will be removed in a "
9✔
934
             "future release of python-control; use "
935
             "sys.frequency_response(omega), or freqresp(sys, omega) in the "
936
             "MATLAB compatibility module instead", FutureWarning)
937
        return self.frequency_response(omega)
9✔
938

939
    # Compute poles and zeros
940
    def poles(self):
9✔
941
        """Compute the poles of a state space system."""
942

943
        return eigvals(self.A).astype(complex) if self.nstates \
9✔
944
            else np.array([])
945

946
    def zeros(self):
9✔
947
        """Compute the zeros of a state space system."""
948

949
        if not self.nstates:
9✔
950
            return np.array([])
×
951

952
        # Use AB08ND from Slycot if it's available, otherwise use
953
        # scipy.lingalg.eigvals().
954
        try:
9✔
955
            from slycot import ab08nd
9✔
956

957
            out = ab08nd(self.A.shape[0], self.B.shape[1], self.C.shape[0],
5✔
958
                         self.A, self.B, self.C, self.D)
959
            nu = out[0]
5✔
960
            if nu == 0:
5✔
961
                return np.array([])
5✔
962
            else:
963
                # Use SciPy generalized eigenvalue function
964
                return sp.linalg.eigvals(out[8][0:nu, 0:nu],
5✔
965
                                         out[9][0:nu, 0:nu]).astype(complex)
966

967
        except ImportError:  # Slycot unavailable. Fall back to SciPy.
4✔
968
            if self.C.shape[0] != self.D.shape[1]:
4✔
969
                raise NotImplementedError(
970
                    "StateSpace.zero only supports systems with the same "
971
                    "number of inputs as outputs.")
972

973
            # This implements the QZ algorithm for finding transmission zeros
974
            # from
975
            # https://dspace.mit.edu/bitstream/handle/1721.1/841/P-0802-06587335.pdf.
976
            # The QZ algorithm solves the generalized eigenvalue problem: given
977
            # `L = [A, B; C, D]` and `M = [I_nxn 0]`, find all finite lambda
978
            # for which there exist nontrivial solutions of the equation
979
            # `Lz - lamba Mz`.
980
            #
981
            # The generalized eigenvalue problem is only solvable if its
982
            # arguments are square matrices.
983
            L = concatenate((concatenate((self.A, self.B), axis=1),
4✔
984
                             concatenate((self.C, self.D), axis=1)), axis=0)
985
            M = pad(eye(self.A.shape[0]), ((0, self.C.shape[0]),
4✔
986
                                           (0, self.B.shape[1])), "constant")
987
            return np.array([x for x in sp.linalg.eigvals(L, M,
4✔
988
                                                          overwrite_a=True)
989
                             if not isinf(x)], dtype=complex)
990

991
    # Feedback around a state space system
992
    def feedback(self, other=1, sign=-1):
9✔
993
        """Feedback interconnection between two LTI objects.
994

995
        Parameters
996
        ----------
997
        other : `InputOutputSystem`
998
            System in the feedback path.
999

1000
        sign : float, optional
1001
            Gain to use in feedback path.  Defaults to -1.
1002

1003
        """
1004
        # Convert the system to state space, if possible
1005
        try:
9✔
1006
            other = _convert_to_statespace(other)
9✔
1007
        except:
9✔
1008
            pass
9✔
1009

1010
        if not isinstance(other, StateSpace):
9✔
1011
            return NonlinearIOSystem.feedback(self, other, sign)
9✔
1012

1013
        # Check to make sure the dimensions are OK
1014
        if self.ninputs != other.noutputs or self.noutputs != other.ninputs:
9✔
1015
            raise ValueError("State space systems don't have compatible "
×
1016
                             "inputs/outputs for feedback.")
1017
        dt = common_timebase(self.dt, other.dt)
9✔
1018

1019
        A1 = self.A
9✔
1020
        B1 = self.B
9✔
1021
        C1 = self.C
9✔
1022
        D1 = self.D
9✔
1023
        A2 = other.A
9✔
1024
        B2 = other.B
9✔
1025
        C2 = other.C
9✔
1026
        D2 = other.D
9✔
1027

1028
        F = eye(self.ninputs) - sign * D2 @ D1
9✔
1029
        if matrix_rank(F) != self.ninputs:
9✔
1030
            raise ValueError(
×
1031
                "I - sign * D2 * D1 is singular to working precision.")
1032

1033
        # Precompute F\D2 and F\C2 (E = inv(F))
1034
        # We can solve two linear systems in one pass, since the
1035
        # coefficients matrix F is the same. Thus, we perform the LU
1036
        # decomposition (cubic runtime complexity) of F only once!
1037
        # The remaining back substitutions are only quadratic in runtime.
1038
        E_D2_C2 = solve(F, concatenate((D2, C2), axis=1))
9✔
1039
        E_D2 = E_D2_C2[:, :other.ninputs]
9✔
1040
        E_C2 = E_D2_C2[:, other.ninputs:]
9✔
1041

1042
        T1 = eye(self.noutputs) + sign * D1 @ E_D2
9✔
1043
        T2 = eye(self.ninputs) + sign * E_D2 @ D1
9✔
1044

1045
        A = concatenate(
9✔
1046
            (concatenate(
1047
                (A1 + sign * B1 @ E_D2 @ C1,
1048
                 sign * B1 @ E_C2), axis=1),
1049
             concatenate(
1050
                 (B2 @ T1 @ C1,
1051
                  A2 + sign * B2 @ D1 @ E_C2), axis=1)),
1052
            axis=0)
1053
        B = concatenate((B1 @ T2, B2 @ D1 @ T2), axis=0)
9✔
1054
        C = concatenate((T1 @ C1, sign * D1 @ E_C2), axis=1)
9✔
1055
        D = D1 @ T2
9✔
1056

1057
        return StateSpace(A, B, C, D, dt)
9✔
1058

1059
    def lft(self, other, nu=-1, ny=-1):
9✔
1060
        """Return the linear fractional transformation.
1061

1062
        A definition of the LFT operator can be found in Appendix A.7,
1063
        page 512 in [1]_.  An alternative definition can be found here:
1064
        https://www.mathworks.com/help/control/ref/lft.html
1065

1066
        Parameters
1067
        ----------
1068
        other : `StateSpace`
1069
            The lower LTI system.
1070
        ny : int, optional
1071
            Dimension of (plant) measurement output.
1072
        nu : int, optional
1073
            Dimension of (plant) control input.
1074

1075
        Returns
1076
        -------
1077
        `StateSpace`
1078

1079
        References
1080
        ----------
1081
        .. [1] S. Skogestad, Multivariable Feedback Control.  Second
1082
           edition, 2005.
1083

1084
        """
1085
        other = _convert_to_statespace(other)
9✔
1086
        # maximal values for nu, ny
1087
        if ny == -1:
9✔
1088
            ny = min(other.ninputs, self.noutputs)
9✔
1089
        if nu == -1:
9✔
1090
            nu = min(other.noutputs, self.ninputs)
9✔
1091
        # dimension check
1092
        # TODO
1093

1094
        dt = common_timebase(self.dt, other.dt)
9✔
1095

1096
        # submatrices
1097
        A = self.A
9✔
1098
        B1 = self.B[:, :self.ninputs - nu]
9✔
1099
        B2 = self.B[:, self.ninputs - nu:]
9✔
1100
        C1 = self.C[:self.noutputs - ny, :]
9✔
1101
        C2 = self.C[self.noutputs - ny:, :]
9✔
1102
        D11 = self.D[:self.noutputs - ny, :self.ninputs - nu]
9✔
1103
        D12 = self.D[:self.noutputs - ny, self.ninputs - nu:]
9✔
1104
        D21 = self.D[self.noutputs - ny:, :self.ninputs - nu]
9✔
1105
        D22 = self.D[self.noutputs - ny:, self.ninputs - nu:]
9✔
1106

1107
        # submatrices
1108
        Abar = other.A
9✔
1109
        Bbar1 = other.B[:, :ny]
9✔
1110
        Bbar2 = other.B[:, ny:]
9✔
1111
        Cbar1 = other.C[:nu, :]
9✔
1112
        Cbar2 = other.C[nu:, :]
9✔
1113
        Dbar11 = other.D[:nu, :ny]
9✔
1114
        Dbar12 = other.D[:nu, ny:]
9✔
1115
        Dbar21 = other.D[nu:, :ny]
9✔
1116
        Dbar22 = other.D[nu:, ny:]
9✔
1117

1118
        # well-posed check
1119
        F = np.block([[np.eye(ny), -D22], [-Dbar11, np.eye(nu)]])
9✔
1120
        if matrix_rank(F) != ny + nu:
9✔
1121
            raise ValueError("LFT not well-posed to working precision.")
×
1122

1123
        # solve for the resulting ss by solving for [y, u] using [x,
1124
        # xbar] and [w1, w2].
1125
        TH = np.linalg.solve(F, np.block(
9✔
1126
            [[C2, np.zeros((ny, other.nstates)),
1127
              D21, np.zeros((ny, other.ninputs - ny))],
1128
             [np.zeros((nu, self.nstates)), Cbar1,
1129
              np.zeros((nu, self.ninputs - nu)), Dbar12]]
1130
        ))
1131
        T11 = TH[:ny, :self.nstates]
9✔
1132
        T12 = TH[:ny, self.nstates: self.nstates + other.nstates]
9✔
1133
        T21 = TH[ny:, :self.nstates]
9✔
1134
        T22 = TH[ny:, self.nstates: self.nstates + other.nstates]
9✔
1135
        H11 = TH[:ny, self.nstates + other.nstates:self.nstates +
9✔
1136
                 other.nstates + self.ninputs - nu]
1137
        H12 = TH[:ny, self.nstates + other.nstates + self.ninputs - nu:]
9✔
1138
        H21 = TH[ny:, self.nstates + other.nstates:self.nstates +
9✔
1139
                 other.nstates + self.ninputs - nu]
1140
        H22 = TH[ny:, self.nstates + other.nstates + self.ninputs - nu:]
9✔
1141

1142
        Ares = np.block([
9✔
1143
            [A + B2 @ T21, B2 @ T22],
1144
            [Bbar1 @ T11, Abar + Bbar1 @ T12]
1145
        ])
1146

1147
        Bres = np.block([
9✔
1148
            [B1 + B2 @ H21, B2 @ H22],
1149
            [Bbar1 @ H11, Bbar2 + Bbar1 @ H12]
1150
        ])
1151

1152
        Cres = np.block([
9✔
1153
            [C1 + D12 @ T21, D12 @ T22],
1154
            [Dbar21 @ T11, Cbar2 + Dbar21 @ T12]
1155
        ])
1156

1157
        Dres = np.block([
9✔
1158
            [D11 + D12 @ H21, D12 @ H22],
1159
            [Dbar21 @ H11, Dbar22 + Dbar21 @ H12]
1160
        ])
1161
        return StateSpace(Ares, Bres, Cres, Dres, dt)
9✔
1162

1163
    def minreal(self, tol=0.0):
9✔
1164
        """Remove unobservable and uncontrollable states.
1165

1166
        Calculate a minimal realization for a state space system,
1167
        removing all unobservable and/or uncontrollable states.
1168

1169
        Parameters
1170
        ----------
1171
        tol : float
1172
            Tolerance for determining whether states are unobservable
1173
            or uncontrollable.
1174

1175
        """
1176
        if self.nstates:
9✔
1177
            try:
5✔
1178
                from slycot import tb01pd
5✔
1179
                B = empty((self.nstates, max(self.ninputs, self.noutputs)))
5✔
1180
                B[:, :self.ninputs] = self.B
5✔
1181
                C = empty((max(self.noutputs, self.ninputs), self.nstates))
5✔
1182
                C[:self.noutputs, :] = self.C
5✔
1183
                A, B, C, nr = tb01pd(self.nstates, self.ninputs, self.noutputs,
5✔
1184
                                     self.A, B, C, tol=tol)
1185
                return StateSpace(A[:nr, :nr], B[:nr, :self.ninputs],
5✔
1186
                                  C[:self.noutputs, :nr], self.D, self.dt)
1187
            except ImportError:
×
1188
                raise TypeError("minreal requires slycot tb01pd")
×
1189
        else:
1190
            return StateSpace(self)
9✔
1191

1192
    def returnScipySignalLTI(self, strict=True):
9✔
1193
        """Return a list of a list of `scipy.signal.lti` objects.
1194

1195
        For instance,
1196

1197
        >>> out = ssobject.returnScipySignalLTI()               # doctest: +SKIP
1198
        >>> out[3][5]                                           # doctest: +SKIP
1199

1200
        is a `scipy.signal.lti` object corresponding to the transfer
1201
        function from the 6th input to the 4th output.
1202

1203
        Parameters
1204
        ----------
1205
        strict : bool, optional
1206
            True (default):
1207
                The timebase `ssobject.dt` cannot be None; it must
1208
                be continuous (0) or discrete (True or > 0).
1209
            False:
1210
              If `ssobject.dt` is None, continuous-time
1211
              `scipy.signal.lti` objects are returned.
1212

1213
        Returns
1214
        -------
1215
        out : list of list of `scipy.signal.StateSpace`
1216
            Continuous time (inheriting from `scipy.signal.lti`)
1217
            or discrete time (inheriting from `scipy.signal.dlti`)
1218
            SISO objects.
1219

1220
        """
1221
        if strict and self.dt is None:
9✔
1222
            raise ValueError("with strict=True, dt cannot be None")
9✔
1223

1224
        if self.dt:
9✔
1225
            kwdt = {'dt': self.dt}
9✔
1226
        else:
1227
            # SciPy convention for continuous-time LTI systems: call without
1228
            # dt keyword argument
1229
            kwdt = {}
9✔
1230

1231
        # Preallocate the output.
1232
        out = [[[] for _ in range(self.ninputs)] for _ in range(self.noutputs)]
9✔
1233

1234
        for i in range(self.noutputs):
9✔
1235
            for j in range(self.ninputs):
9✔
1236
                out[i][j] = signalStateSpace(asarray(self.A),
9✔
1237
                                             asarray(self.B[:, j:j + 1]),
1238
                                             asarray(self.C[i:i + 1, :]),
1239
                                             asarray(self.D[i:i + 1, j:j + 1]),
1240
                                             **kwdt)
1241

1242
        return out
9✔
1243

1244
    def append(self, other):
9✔
1245
        """Append a second model to the present model.
1246

1247
        The second model is converted to state-space if necessary, inputs and
1248
        outputs are appended and their order is preserved.
1249

1250
        Parameters
1251
        ----------
1252
        other : `StateSpace` or `TransferFunction`
1253
            System to be appended.
1254

1255
        Returns
1256
        -------
1257
        sys : `StateSpace`
1258
            System model with `other` appended to `self`.
1259

1260
        """
1261
        if not isinstance(other, StateSpace):
9✔
1262
            other = _convert_to_statespace(other)
9✔
1263

1264
        self.dt = common_timebase(self.dt, other.dt)
9✔
1265

1266
        n = self.nstates + other.nstates
9✔
1267
        m = self.ninputs + other.ninputs
9✔
1268
        p = self.noutputs + other.noutputs
9✔
1269
        A = zeros((n, n))
9✔
1270
        B = zeros((n, m))
9✔
1271
        C = zeros((p, n))
9✔
1272
        D = zeros((p, m))
9✔
1273
        A[:self.nstates, :self.nstates] = self.A
9✔
1274
        A[self.nstates:, self.nstates:] = other.A
9✔
1275
        B[:self.nstates, :self.ninputs] = self.B
9✔
1276
        B[self.nstates:, self.ninputs:] = other.B
9✔
1277
        C[:self.noutputs, :self.nstates] = self.C
9✔
1278
        C[self.noutputs:, self.nstates:] = other.C
9✔
1279
        D[:self.noutputs, :self.ninputs] = self.D
9✔
1280
        D[self.noutputs:, self.ninputs:] = other.D
9✔
1281
        return StateSpace(A, B, C, D, self.dt)
9✔
1282

1283
    def __getitem__(self, key):
9✔
1284
        """Array style access"""
1285
        if not isinstance(key, Iterable) or len(key) != 2:
9✔
1286
            raise IOError("must provide indices of length 2 for state space")
9✔
1287

1288
        # Convert signal names to integer offsets
1289
        iomap = NamedSignal(self.D, self.output_labels, self.input_labels)
9✔
1290
        indices = iomap._parse_key(key, level=1)  # ignore index checks
9✔
1291
        outdx, output_labels = _process_subsys_index(
9✔
1292
            indices[0], self.output_labels)
1293
        inpdx, input_labels = _process_subsys_index(
9✔
1294
            indices[1], self.input_labels)
1295

1296
        sysname = config.defaults['iosys.indexed_system_name_prefix'] + \
9✔
1297
            self.name + config.defaults['iosys.indexed_system_name_suffix']
1298
        return StateSpace(
9✔
1299
            self.A, self.B[:, inpdx], self.C[outdx, :],
1300
            self.D[outdx, :][:, inpdx], self.dt,
1301
            name=sysname, inputs=input_labels, outputs=output_labels)
1302

1303
    def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
9✔
1304
               name=None, copy_names=True, **kwargs):
1305
        """Convert a continuous-time system to discrete time.
1306

1307
        Creates a discrete-time system from a continuous-time system by
1308
        sampling.  Multiple methods of conversion are supported.
1309

1310
        Parameters
1311
        ----------
1312
        Ts : float
1313
            Sampling period.
1314
        method : {'gbt', 'bilinear', 'euler', 'backward_diff', 'zoh'}
1315
            Method to use for sampling:
1316

1317
            * 'gbt': generalized bilinear transformation
1318
            * 'backward_diff': Backwards difference ('gbt' with alpha=1.0)
1319
            * 'bilinear' (or 'tustin'): Tustin's approximation ('gbt' with
1320
              alpha=0.5)
1321
            * 'euler': Euler (or forward difference) method ('gbt' with
1322
              alpha=0)
1323
            * 'zoh': zero-order hold (default)
1324
        alpha : float within [0, 1]
1325
            The generalized bilinear transformation weighting parameter,
1326
            which should only be specified with method='gbt', and is
1327
            ignored otherwise.
1328
        prewarp_frequency : float within [0, infinity)
1329
            The frequency [rad/s] at which to match with the input
1330
            continuous-time system's magnitude and phase (the gain = 1
1331
            crossover frequency, for example). Should only be specified
1332
            with `method` = 'bilinear' or 'gbt' with `alpha` = 0.5 and
1333
            ignored otherwise.
1334
        name : string, optional
1335
            Set the name of the sampled system.  If not specified and if
1336
            `copy_names` is False, a generic name 'sys[id]' is
1337
            generated with a unique integer id.  If `copy_names` is
1338
            True, the new system name is determined by adding the
1339
            prefix and suffix strings in
1340
            `config.defaults['iosys.sampled_system_name_prefix']` and
1341
            `config.defaults['iosys.sampled_system_name_suffix']`, with
1342
            the default being to add the suffix '$sampled'.
1343
        copy_names : bool, Optional
1344
            If True, copy the names of the input signals, output
1345
            signals, and states to the sampled system.
1346

1347
        Returns
1348
        -------
1349
        sysd : `StateSpace`
1350
            Discrete-time system, with sampling rate `Ts`.
1351

1352
        Other Parameters
1353
        ----------------
1354
        inputs : int, list of str or None, optional
1355
            Description of the system inputs.  If not specified, the
1356
            original system inputs are used.  See `InputOutputSystem` for
1357
            more information.
1358
        outputs : int, list of str or None, optional
1359
            Description of the system outputs.  Same format as `inputs`.
1360
        states : int, list of str, or None, optional
1361
            Description of the system states.  Same format as `inputs`.
1362

1363
        Notes
1364
        -----
1365
        Uses `scipy.signal.cont2discrete`.
1366

1367
        Examples
1368
        --------
1369
        >>> G = ct.ss(0, 1, 1, 0)
1370
        >>> sysd = G.sample(0.5, method='bilinear')
1371

1372
        """
1373
        if not self.isctime():
9✔
1374
            raise ValueError("System must be continuous-time system")
×
1375
        if prewarp_frequency is not None:
9✔
1376
            if method in ('bilinear', 'tustin') or \
9✔
1377
                    (method == 'gbt' and alpha == 0.5):
1378
                Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency
9✔
1379
            else:
1380
                warn('prewarp_frequency ignored: incompatible conversion')
9✔
1381
                Twarp = Ts
9✔
1382
        else:
1383
            Twarp = Ts
9✔
1384
        sys = (self.A, self.B, self.C, self.D)
9✔
1385
        Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha)
9✔
1386
        sysd = StateSpace(Ad, Bd, C, D, Ts)
9✔
1387
        # copy over the system name, inputs, outputs, and states
1388
        if copy_names:
9✔
1389
            sysd._copy_names(self, prefix_suffix_name='sampled')
9✔
1390
            if name is not None:
9✔
1391
                sysd.name = name
9✔
1392
        # pass desired signal names if names were provided
1393
        return StateSpace(sysd, **kwargs)
9✔
1394

1395
    def dcgain(self, warn_infinite=False):
9✔
1396
        """Return the zero-frequency ("DC") gain.
1397

1398
        The zero-frequency gain of a continuous-time state-space
1399
        system is given by:
1400

1401
        .. math: G(0) = - C A^{-1} B + D
1402

1403
        and of a discrete-time state-space system by:
1404

1405
        .. math: G(1) = C (I - A)^{-1} B + D
1406

1407
        Parameters
1408
        ----------
1409
        warn_infinite : bool, optional
1410
            By default, don't issue a warning message if the zero-frequency
1411
            gain is infinite.  Setting `warn_infinite` to generate the
1412
            warning message.
1413

1414
        Returns
1415
        -------
1416
        gain : (noutputs, ninputs) ndarray or scalar
1417
            Array or scalar value for SISO systems, depending on
1418
            `config.defaults['control.squeeze_frequency_response']`.  The
1419
            value of the array elements or the scalar is either the
1420
            zero-frequency (or DC) gain, or `inf`, if the frequency
1421
            response is singular.
1422

1423
            For real valued systems, the empty imaginary part of the
1424
            complex zero-frequency response is discarded and a real array or
1425
            scalar is returned.
1426

1427
        """
1428
        return self._dcgain(warn_infinite)
9✔
1429

1430
    # TODO: decide if we need this function (already in NonlinearIOSystem
1431
    def dynamics(self, t, x, u=None, params=None):
9✔
1432
        """Compute the dynamics of the system.
1433

1434
        Given input `u` and state `x`, returns the dynamics of the state-space
1435
        system. If the system is continuous, returns the time derivative dx/dt
1436

1437
            dx/dt = A x + B u
1438

1439
        where A and B are the state-space matrices of the system. If the
1440
        system is discrete time, returns the next value of `x`:
1441

1442
            x[t+dt] = A x[t] + B u[t]
1443

1444
        The inputs `x` and `u` must be of the correct length for the system.
1445

1446
        The first argument `t` is ignored because `StateSpace` systems
1447
        are time-invariant. It is included so that the dynamics can be passed
1448
        to numerical integrators, such as `scipy.integrate.solve_ivp`
1449
        and for consistency with `InputOutputSystem` models.
1450

1451
        Parameters
1452
        ----------
1453
        t : float (ignored)
1454
            Time.
1455
        x : array_like
1456
            Current state.
1457
        u : array_like (optional)
1458
            Input, zero if omitted.
1459

1460
        Returns
1461
        -------
1462
        dx/dt or x[t+dt] : ndarray
1463

1464
        """
1465
        if params is not None:
9✔
1466
            warn("params keyword ignored for StateSpace object")
9✔
1467

1468
        x = np.reshape(x, (-1, 1))  # force to a column in case matrix
9✔
1469
        if np.size(x) != self.nstates:
9✔
1470
            raise ValueError("len(x) must be equal to number of states")
9✔
1471
        if u is None:
9✔
1472
            return (self.A @ x).reshape((-1,))  # return as row vector
9✔
1473
        else:  # received t, x, and u, ignore t
1474
            u = np.reshape(u, (-1, 1))  # force to column in case matrix
9✔
1475
            if np.size(u) != self.ninputs:
9✔
1476
                raise ValueError("len(u) must be equal to number of inputs")
9✔
1477
            return (self.A @ x).reshape((-1,)) \
9✔
1478
                + (self.B @ u).reshape((-1,))  # return as row vector
1479

1480
    # TODO: decide if we need this function (already in NonlinearIOSystem
1481
    def output(self, t, x, u=None, params=None):
9✔
1482
        """Compute the output of the system.
1483

1484
        Given input `u` and state `x`, returns the output `y` of the
1485
        state-space system:
1486

1487
            y = C x + D u
1488

1489
        where A and B are the state-space matrices of the system.
1490

1491
        The first argument `t` is ignored because `StateSpace` systems
1492
        are time-invariant. It is included so that the dynamics can be passed
1493
        to most numerical integrators, such as SciPy's `integrate.solve_ivp`
1494
        and for consistency with `InputOutputSystem` models.
1495

1496
        The inputs `x` and `u` must be of the correct length for the system.
1497

1498
        Parameters
1499
        ----------
1500
        t : float (ignored)
1501
            Time.
1502
        x : array_like
1503
            Current state.
1504
        u : array_like (optional)
1505
            Input (zero if omitted).
1506

1507
        Returns
1508
        -------
1509
        y : ndarray
1510

1511
        """
1512
        if params is not None:
9✔
1513
            warn("params keyword ignored for StateSpace object")
9✔
1514

1515
        x = np.reshape(x, (-1, 1))  # force to a column in case matrix
9✔
1516
        if np.size(x) != self.nstates:
9✔
1517
            raise ValueError("len(x) must be equal to number of states")
9✔
1518

1519
        if u is None:
9✔
1520
            return (self.C @ x).reshape((-1,))  # return as row vector
9✔
1521
        else:  # received t, x, and u, ignore t
1522
            u = np.reshape(u, (-1, 1))  # force to a column in case matrix
9✔
1523
            if np.size(u) != self.ninputs:
9✔
1524
                raise ValueError("len(u) must be equal to number of inputs")
9✔
1525
            return (self.C @ x).reshape((-1,)) \
9✔
1526
                + (self.D @ u).reshape((-1,))  # return as row vector
1527

1528
    # convenience alias, import needs submodule to avoid circular imports
1529
    initial_response = control.timeresp.initial_response
9✔
1530

1531

1532
class LinearICSystem(InterconnectedSystem, StateSpace):
9✔
1533
    """Interconnection of a set of linear input/output systems.
1534

1535
    This class is used to implement a system that is an interconnection of
1536
    linear input/output systems.  It has all of the structure of an
1537
    `InterconnectedSystem`, but also maintains the required
1538
    elements of the `StateSpace` class structure, allowing it to be
1539
    passed to functions that expect a `StateSpace` system.
1540

1541
    This class is generated using `interconnect` and
1542
    not called directly.
1543

1544
    """
1545

1546
    def __init__(self, io_sys, ss_sys=None, connection_type=None):
9✔
1547
        #
1548
        # Because this is a "hybrid" object, the initialization proceeds in
1549
        # stages.  We first create an empty InputOutputSystem of the
1550
        # appropriate size, then copy over the elements of the
1551
        # InterconnectedSystem class.  From there we compute the
1552
        # linearization of the system (if needed) and then populate the
1553
        # StateSpace parameters.
1554
        #
1555
        # Create the (essentially empty) I/O system object
1556
        InputOutputSystem.__init__(
9✔
1557
            self, name=io_sys.name, inputs=io_sys.ninputs,
1558
            outputs=io_sys.noutputs, states=io_sys.nstates, dt=io_sys.dt)
1559

1560
        # Copy over the attributes from the interconnected system
1561
        self.syslist = io_sys.syslist
9✔
1562
        self.syslist_index = io_sys.syslist_index
9✔
1563
        self.state_offset = io_sys.state_offset
9✔
1564
        self.input_offset = io_sys.input_offset
9✔
1565
        self.output_offset = io_sys.output_offset
9✔
1566
        self.connect_map = io_sys.connect_map
9✔
1567
        self.input_map = io_sys.input_map
9✔
1568
        self.output_map = io_sys.output_map
9✔
1569
        self.params = io_sys.params
9✔
1570
        self.connection_type = connection_type
9✔
1571

1572
        # If we didn't' get a state space system, linearize the full system
1573
        if ss_sys is None:
9✔
1574
            ss_sys = self.linearize(0, 0)
9✔
1575

1576
        # Initialize the state space object
1577
        StateSpace.__init__(
9✔
1578
            self, ss_sys, name=io_sys.name, inputs=io_sys.input_labels,
1579
            outputs=io_sys.output_labels, states=io_sys.state_labels,
1580
            params=io_sys.params, remove_useless_states=False)
1581

1582
    # Use StateSpace.__call__ to evaluate at a given complex value
1583
    def __call__(self, *args, **kwargs):
9✔
1584
        return StateSpace.__call__(self, *args, **kwargs)
9✔
1585

1586
    def __str__(self):
9✔
1587
        string = InterconnectedSystem.__str__(self) + "\n\n"
9✔
1588
        string += "\n\n".join([
9✔
1589
            "{} = {}".format(Mvar,
1590
                               "\n    ".join(str(M).splitlines()))
1591
            for Mvar, M in zip(["A", "B", "C", "D"],
1592
                               [self.A, self.B, self.C, self.D])])
1593
        return string
9✔
1594

1595
    # Use InputOutputSystem repr for 'eval' since we can't recreate structure
1596
    # (without this, StateSpace._repr_eval_ gets used...)
1597
    def _repr_eval_(self):
9✔
1598
        return InputOutputSystem._repr_eval_(self)
×
1599

1600
    def _repr_html_(self):
9✔
1601
        syssize = self.nstates + max(self.noutputs, self.ninputs)
×
1602
        if syssize > config.defaults['statesp.latex_maxsize']:
×
1603
            return None
×
1604
        elif config.defaults['statesp.latex_repr_type'] == 'partitioned':
×
1605
            return InterconnectedSystem._repr_info_(self, html=True) + \
×
1606
                "\n" + StateSpace._latex_partitioned(self)
1607
        elif config.defaults['statesp.latex_repr_type'] == 'separate':
×
1608
            return InterconnectedSystem._repr_info_(self, html=True) + \
×
1609
                "\n" + StateSpace._latex_separate(self)
1610
        else:
1611
            raise ValueError(
×
1612
                "Unknown statesp.latex_repr_type '{cfg}'".format(
1613
                    cfg=config.defaults['statesp.latex_repr_type']))
1614

1615
    # The following text needs to be replicated from StateSpace in order for
1616
    # this entry to show up properly in sphinx documentation (not sure why,
1617
    # but it was the only way to get it to work).
1618
    #
1619
    #: Deprecated attribute; use `nstates` instead.
1620
    #:
1621
    #: The `state` attribute was used to store the number of states for : a
1622
    #: state space system.  It is no longer used.  If you need to access the
1623
    #: number of states, use `nstates`.
1624
    states = property(StateSpace._get_states, StateSpace._set_states)
9✔
1625

1626

1627
# Define a state space object that is an I/O system
1628
def ss(*args, **kwargs):
9✔
1629
    r"""ss(A, B, C, D[, dt])
1630

1631
    Create a state space system.
1632

1633
    The function accepts either 1, 4 or 5 positional parameters:
1634

1635
    ``ss(sys)``
1636

1637
        Convert a linear system into space system form. Always creates a
1638
        new system, even if `sys` is already a state space system.
1639

1640
    ``ss(A, B, C, D)``
1641

1642
        Create a state space system from the matrices of its state and
1643
        output equations:
1644

1645
        .. math::
1646

1647
            dx/dt &= A x + B u \\
1648
                y &= C x + D  u
1649

1650
    ``ss(A, B, C, D, dt)``
1651

1652
        Create a discrete-time state space system from the matrices of
1653
        its state and output equations:
1654

1655
        .. math::
1656

1657
            x[k+1] &= A x[k] + B u[k] \\
1658
              y[k] &= C x[k] + D u[k]
1659

1660
        The matrices can be given as 2D array_like data types.  For SISO
1661
        systems, `B` and `C` can be given as 1D arrays and D can be given
1662
        as a scalar.
1663

1664

1665
    ``ss(*args, inputs=['u1', ..., 'up'], outputs=['y1', ..., 'yq'], states=['x1', ..., 'xn'])``
1666
        Create a system with named input, output, and state signals.
1667

1668
    Parameters
1669
    ----------
1670
    sys : `StateSpace` or `TransferFunction`
1671
        A linear system.
1672
    A, B, C, D : array_like or string
1673
        System, control, output, and feed forward matrices.
1674
    dt : None, True or float, optional
1675
        System timebase. 0 (default) indicates continuous time, True
1676
        indicates discrete time with unspecified sampling time, positive
1677
        number is discrete time with specified sampling time, None
1678
        indicates unspecified timebase (either continuous or discrete time).
1679
    remove_useless_states : bool, optional
1680
        If True, remove states that have no effect on the input/output
1681
        dynamics.  If not specified, the value is read from
1682
        `config.defaults['statesp.remove_useless_states']` (default = False).
1683
    method : str, optional
1684
        Set the method used for converting a transfer function to a state
1685
        space system.  Current methods are 'slycot' and 'scipy'.  If set to
1686
        None (default), try 'slycot' first and then 'scipy' (SISO only).
1687

1688
    Returns
1689
    -------
1690
    out : `StateSpace`
1691
        Linear input/output system.
1692

1693
    Other Parameters
1694
    ----------------
1695
    inputs, outputs, states : str, or list of str, optional
1696
        List of strings that name the individual signals.  If this parameter
1697
        is not given or given as None, the signal names will be of the
1698
        form 's[i]' (where 's' is one of 'u', 'y', or 'x'). See
1699
        `InputOutputSystem` for more information.
1700
    input_prefix, output_prefix, state_prefix : string, optional
1701
        Set the prefix for input, output, and state signals.  Defaults =
1702
        'u', 'y', 'x'.
1703
    name : string, optional
1704
        System name (used for specifying signals). If unspecified, a generic
1705
        name 'sys[id]' is generated with a unique integer id.
1706

1707
    Raises
1708
    ------
1709
    ValueError
1710
        If matrix sizes are not self-consistent.
1711

1712
    See Also
1713
    --------
1714
    StateSpace, nlsys, tf, ss2tf, tf2ss, zpk
1715

1716
    Notes
1717
    -----
1718
    If a transfer function is passed as the sole positional argument, the
1719
    system will be converted to state space form in the same way as calling
1720
    `tf2ss`.  The `method` keyword can be used to select the
1721
    method for conversion.
1722

1723
    Examples
1724
    --------
1725
    Create a linear I/O system object from matrices:
1726

1727
    >>> G = ct.ss([[-1, -2], [3, -4]], [[5], [7]], [[6, 8]], [[9]])
1728

1729
    Convert a transfer function to a state space system:
1730

1731
    >>> sys_tf = ct.tf([2.], [1., 3])
1732
    >>> sys2 = ct.ss(sys_tf)
1733

1734
    """
1735
    # See if this is a nonlinear I/O system (legacy usage)
1736
    if len(args) > 0 and (hasattr(args[0], '__call__') or args[0] is None) \
9✔
1737
       and not isinstance(args[0], (InputOutputSystem, LTI)):
1738
        # Function as first (or second) argument => assume nonlinear IO system
1739
        warn("using ss() to create nonlinear I/O systems is deprecated; "
9✔
1740
             "use nlsys()", FutureWarning)
1741
        return NonlinearIOSystem(*args, **kwargs)
9✔
1742

1743
    elif len(args) == 4 or len(args) == 5:
9✔
1744
        # Create a state space function from A, B, C, D[, dt]
1745
        sys = StateSpace(*args, **kwargs)
9✔
1746

1747
    elif len(args) == 1:
9✔
1748
        sys = args[0]
9✔
1749
        if isinstance(sys, LTI):
9✔
1750
            # Check for system with no states and specified state names
1751
            if sys.nstates is None and 'states' in kwargs:
9✔
1752
                warn("state labels specified for "
9✔
1753
                     "non-unique state space realization")
1754

1755
            # Allow method to be specified (e.g., tf2ss)
1756
            method = kwargs.pop('method', None)
9✔
1757

1758
            # Create a state space system from an LTI system
1759
            sys = StateSpace(
9✔
1760
                _convert_to_statespace(
1761
                    sys, method=method,
1762
                    use_prefix_suffix=not sys._generic_name_check()),
1763
                **kwargs)
1764

1765
        else:
1766
            raise TypeError("ss(sys): sys must be a StateSpace or "
9✔
1767
                            "TransferFunction object.  It is %s." % type(sys))
1768
    else:
1769
        raise TypeError(
9✔
1770
            "Needs 1, 4, or 5 arguments; received %i." % len(args))
1771

1772
    return sys
9✔
1773

1774

1775
# Convert a state space system into an input/output system (wrapper)
1776
def ss2io(*args, **kwargs):
9✔
1777
    """ss2io(sys[, ...])
1778

1779
    Create an I/O system from a state space linear system.
1780

1781
    .. deprecated:: 0.10.0
1782
        This function will be removed in a future version of python-control.
1783
        The `ss` function can be used directly to produce an I/O system.
1784

1785
    Create an `StateSpace` system with the given signal
1786
    and system names.  See `ss` for more details.
1787
    """
1788
    warn("ss2io() is deprecated; use ss()", FutureWarning)
9✔
1789
    return StateSpace(*args, **kwargs)
9✔
1790

1791

1792
# Convert a transfer function into an input/output system (wrapper)
1793
def tf2io(*args, **kwargs):
9✔
1794
    """tf2io(sys[, ...])
1795

1796
    Convert a transfer function into an I/O system.
1797

1798
    .. deprecated:: 0.10.0
1799
        This function will be removed in a future version of python-control.
1800
        The `tf2ss` function can be used to produce a state space I/O system.
1801

1802
    The function accepts either 1 or 2 parameters:
1803

1804
    ``tf2io(sys)``
1805

1806
        Convert a linear system into space space form. Always creates
1807
        a new system, even if `sys` is already a `StateSpace` object.
1808

1809
    ``tf2io(num, den)``
1810

1811
        Create a linear I/O system from its numerator and denominator
1812
        polynomial coefficients.
1813

1814
        For details see: `tf`.
1815

1816
    Parameters
1817
    ----------
1818
    sys : `StateSpace` or `TransferFunction`
1819
        A linear system.
1820
    num : array_like, or list of list of array_like
1821
        Polynomial coefficients of the numerator.
1822
    den : array_like, or list of list of array_like
1823
        Polynomial coefficients of the denominator.
1824

1825
    Returns
1826
    -------
1827
    out : `StateSpace`
1828
        New I/O system (in state space form).
1829

1830
    Other Parameters
1831
    ----------------
1832
    inputs, outputs : str, or list of str, optional
1833
        List of strings that name the individual signals of the transformed
1834
        system.  If not given, the inputs and outputs are the same as the
1835
        original system.
1836
    name : string, optional
1837
        System name. If unspecified, a generic name 'sys[id]' is generated
1838
        with a unique integer id.
1839

1840
    Raises
1841
    ------
1842
    ValueError
1843
        If `num` and `den` have invalid or unequal dimensions, or if an
1844
        invalid number of arguments is passed in.
1845
    TypeError
1846
        If `num` or `den` are of incorrect type, or if `sys` is not a
1847
        `TransferFunction` object.
1848

1849
    See Also
1850
    --------
1851
    ss2io, tf2ss
1852

1853
    Examples
1854
    --------
1855
    >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
1856
    >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
1857
    >>> sys1 = ct.tf2ss(num, den)
1858

1859
    >>> sys_tf = ct.tf(num, den)
1860
    >>> G = ct.tf2ss(sys_tf)
1861
    >>> G.ninputs, G.noutputs, G.nstates
1862
    (2, 2, 8)
1863

1864
    """
1865
    warn("tf2io() is deprecated; use tf2ss() or tf()", FutureWarning)
9✔
1866
    return tf2ss(*args, **kwargs)
9✔
1867

1868

1869
def tf2ss(*args, **kwargs):
9✔
1870
    """tf2ss(sys)
1871

1872
    Transform a transfer function to a state space system.
1873

1874
    The function accepts either 1 or 2 parameters:
1875

1876
    ``tf2ss(sys)``
1877

1878
        Convert a transfer function into space space form.  Equivalent to
1879
        `ss(sys)`.
1880

1881
    ``tf2ss(num, den)``
1882

1883
        Create a state space system from its numerator and denominator
1884
        polynomial coefficients.
1885

1886
        For details see: `tf`.
1887

1888
    Parameters
1889
    ----------
1890
    sys : `StateSpace` or `TransferFunction`
1891
        A linear system.
1892
    num : array_like, or list of list of array_like
1893
        Polynomial coefficients of the numerator.
1894
    den : array_like, or list of list of array_like
1895
        Polynomial coefficients of the denominator.
1896

1897
    Returns
1898
    -------
1899
    out : `StateSpace`
1900
        New linear system in state space form.
1901

1902
    Other Parameters
1903
    ----------------
1904
    inputs, outputs : str, or list of str, optional
1905
        List of strings that name the individual signals of the transformed
1906
        system.  If not given, the inputs and outputs are the same as the
1907
        original system.
1908
    name : string, optional
1909
        System name. If unspecified, a generic name 'sys[id]' is generated
1910
        with a unique integer id.
1911
    method : str, optional
1912
        Set the method used for computing the result.  Current methods are
1913
        'slycot' and 'scipy'.  If set to None (default), try 'slycot'
1914
        first and then 'scipy' (SISO only).
1915

1916
    Raises
1917
    ------
1918
    ValueError
1919
        If `num` and `den` have invalid or unequal dimensions, or if an
1920
        invalid number of arguments is passed in.
1921
    TypeError
1922
        If `num` or `den` are of incorrect type, or if `sys` is not a
1923
        `TransferFunction` object.
1924

1925
    See Also
1926
    --------
1927
    ss, tf, ss2tf
1928

1929
    Notes
1930
    -----
1931
    The `slycot` routine used to convert a transfer function into state space
1932
    form appears to have a bug and in some (rare) instances may not return
1933
    a system with the same poles as the input transfer function.  For SISO
1934
    systems, setting `method` = 'scipy' can be used as an alternative.
1935

1936
    Examples
1937
    --------
1938
    >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
1939
    >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
1940
    >>> sys1 = ct.tf2ss(num, den)
1941

1942
    >>> sys_tf = ct.tf(num, den)
1943
    >>> sys2 = ct.tf2ss(sys_tf)
1944

1945
    """
1946

1947
    from .xferfcn import TransferFunction
9✔
1948
    if len(args) == 2 or len(args) == 3:
9✔
1949
        # Assume we were given the num, den
1950
        return StateSpace(
5✔
1951
            _convert_to_statespace(TransferFunction(*args)), **kwargs)
1952

1953
    elif len(args) == 1:
9✔
1954
        return ss(*args, **kwargs)
9✔
1955

1956
    else:
1957
        raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))
×
1958

1959

1960
def ssdata(sys):
9✔
1961
    """
1962
    Return state space data objects for a system.
1963

1964
    Parameters
1965
    ----------
1966
    sys : `StateSpace` or `TransferFunction`
1967
        LTI system whose data will be returned.
1968

1969
    Returns
1970
    -------
1971
    A, B, C, D : ndarray
1972
        State space data for the system.
1973

1974
    """
1975
    ss = _convert_to_statespace(sys)
9✔
1976
    return ss.A, ss.B, ss.C, ss.D
9✔
1977

1978

1979
# TODO: combine with sysnorm?
1980
def linfnorm(sys, tol=1e-10):
9✔
1981
    """L-infinity norm of a linear system.
1982

1983
    Parameters
1984
    ----------
1985
    sys : `StateSpace` or `TransferFunction`
1986
      System to evaluate L-infinity norm of.
1987
    tol : real scalar
1988
      Tolerance on norm estimate.
1989

1990
    Returns
1991
    -------
1992
    gpeak : non-negative scalar
1993
      L-infinity norm.
1994
    fpeak : non-negative scalar
1995
      Frequency, in rad/s, at which gpeak occurs.
1996

1997
    See Also
1998
    --------
1999
    slycot.ab13dd
2000

2001
    Notes
2002
    -----
2003
    For stable systems, the L-infinity and H-infinity norms are equal;
2004
    for unstable systems, the H-infinity norm is infinite, while the
2005
    L-infinity norm is finite if the system has no poles on the
2006
    imaginary axis.
2007

2008
    """
2009
    if ab13dd is None:
5✔
2010
        raise ControlSlycot("Can't find slycot module ab13dd")
×
2011

2012
    a, b, c, d = ssdata(_convert_to_statespace(sys))
5✔
2013
    e = np.eye(a.shape[0])
5✔
2014

2015
    n = a.shape[0]
5✔
2016
    m = b.shape[1]
5✔
2017
    p = c.shape[0]
5✔
2018

2019
    if n == 0:
5✔
2020
        # ab13dd doesn't accept empty A, B, C, D;
2021
        # static gain case is easy enough to compute
2022
        gpeak = scipy.linalg.svdvals(d)[0]
5✔
2023
        # max SVD is constant with freq; arbitrarily choose 0 as peak
2024
        fpeak = 0
5✔
2025
        return gpeak, fpeak
5✔
2026

2027
    dico = 'C' if sys.isctime() else 'D'
5✔
2028
    jobe = 'I'
5✔
2029
    equil = 'S'
5✔
2030
    jobd = 'Z' if all(0 == d.flat) else 'D'
5✔
2031

2032
    gpeak, fpeak = ab13dd(dico, jobe, equil, jobd, n, m, p, a, e, b, c, d, tol)
5✔
2033

2034
    if dico=='D':
5✔
2035
        fpeak /= sys.dt
5✔
2036

2037
    return gpeak, fpeak
5✔
2038

2039

2040
def rss(states=1, outputs=1, inputs=1, strictly_proper=False, **kwargs):
9✔
2041
    """Create a stable random state space object.
2042

2043
    Parameters
2044
    ----------
2045
    states, outputs, inputs : int, list of str, or None
2046
        Description of the system states, outputs, and inputs. This can be
2047
        given as an integer count or as a list of strings that name the
2048
        individual signals.  If an integer count is specified, the names of
2049
        the signal will be of the form 's[i]' (where 's' is one of 'x',
2050
        'y', or 'u').
2051
    strictly_proper : bool, optional
2052
        If set to True, returns a proper system (no direct term).
2053
    dt : None, True or float, optional
2054
        System timebase. 0 (default) indicates continuous time, True
2055
        indicates discrete time with unspecified sampling time, positive
2056
        number is discrete time with specified sampling time, None
2057
        indicates unspecified timebase (either continuous or discrete time).
2058
    name : string, optional
2059
        System name (used for specifying signals). If unspecified, a generic
2060
        name 'sys[id]' is generated with a unique integer id.
2061

2062
    Returns
2063
    -------
2064
    sys : `StateSpace`
2065
        The randomly created linear system.
2066

2067
    Raises
2068
    ------
2069
    ValueError
2070
        If any input is not a positive integer.
2071

2072
    Notes
2073
    -----
2074
    If the number of states, inputs, or outputs is not specified, then the
2075
    missing numbers are assumed to be 1.  If `dt` is not specified or is
2076
    given as 0 or None, the poles of the returned system will always have a
2077
    negative real part.  If `dt` is True or a positive float, the poles of
2078
    the returned system will have magnitude less than 1.
2079

2080
    """
2081
    # Process keyword arguments
2082
    kwargs.update({'states': states, 'outputs': outputs, 'inputs': inputs})
9✔
2083
    name, inputs, outputs, states, dt = _process_iosys_keywords(kwargs)
9✔
2084

2085
    # Figure out the size of the system
2086
    nstates, _ = _process_signal_list(states)
9✔
2087
    ninputs, _ = _process_signal_list(inputs)
9✔
2088
    noutputs, _ = _process_signal_list(outputs)
9✔
2089

2090
    sys = _rss_generate(
9✔
2091
        nstates, ninputs, noutputs, 'c' if not dt else 'd', name=name,
2092
        strictly_proper=strictly_proper)
2093

2094
    return StateSpace(
9✔
2095
        sys, name=name, states=states, inputs=inputs, outputs=outputs, dt=dt,
2096
        **kwargs)
2097

2098

2099
def drss(*args, **kwargs):
9✔
2100
    """
2101
    drss([states, outputs, inputs, strictly_proper])
2102

2103
    Create a stable, discrete-time, random state space system.
2104

2105
    Create a stable *discrete-time* random state space object.  This
2106
    function calls `rss` using either the `dt` keyword provided by
2107
    the user or `dt` = True if not specified.
2108

2109
    Examples
2110
    --------
2111
    >>> G = ct.drss(states=4, outputs=2, inputs=1)
2112
    >>> G.ninputs, G.noutputs, G.nstates
2113
    (1, 2, 4)
2114
    >>> G.isdtime()
2115
    True
2116

2117

2118
    """
2119
    # Make sure the timebase makes sense
2120
    if 'dt' in kwargs:
9✔
2121
        dt = kwargs['dt']
9✔
2122

2123
        if dt == 0:
9✔
2124
            raise ValueError("drss called with continuous timebase")
9✔
2125
        elif dt is None:
9✔
2126
            warn("drss called with unspecified timebase; "
9✔
2127
                 "system may be interpreted as continuous time")
2128
            kwargs['dt'] = True     # force rss to generate discrete-time sys
9✔
2129
    else:
2130
        dt = True
9✔
2131
        kwargs['dt'] = True
9✔
2132

2133
    # Create the system
2134
    sys = rss(*args, **kwargs)
9✔
2135

2136
    # Reset the timebase (in case it was specified as None)
2137
    sys.dt = dt
9✔
2138

2139
    return sys
9✔
2140

2141

2142
# Summing junction
2143
def summing_junction(
9✔
2144
        inputs=None, output=None, dimension=None, prefix='u', **kwargs):
2145
    """Create a summing junction as an input/output system.
2146

2147
    This function creates a static input/output system that outputs the sum
2148
    of the inputs, potentially with a change in sign for each individual
2149
    input.  The input/output system that is created by this function can be
2150
    used as a component in the `interconnect` function.
2151

2152
    Parameters
2153
    ----------
2154
    inputs : int, string or list of strings
2155
        Description of the inputs to the summing junction.  This can be
2156
        given as an integer count, a string, or a list of strings. If an
2157
        integer count is specified, the names of the input signals will be
2158
        of the form 'u[i]'.
2159
    output : string, optional
2160
        Name of the system output.  If not specified, the output will be 'y'.
2161
    dimension : int, optional
2162
        The dimension of the summing junction.  If the dimension is set to a
2163
        positive integer, a multi-input, multi-output summing junction will be
2164
        created.  The input and output signal names will be of the form
2165
        '<signal>[i]' where 'signal' is the input/output signal name specified
2166
        by the `inputs` and `output` keywords.  Default value is None.
2167
    name : string, optional
2168
        System name (used for specifying signals). If unspecified, a generic
2169
        name 'sys[id]' is generated with a unique integer id.
2170
    prefix : string, optional
2171
        If `inputs` is an integer, create the names of the states using the
2172
        given prefix (default = 'u').  The names of the input will be of the
2173
        form 'prefix[i]'.
2174

2175
    Returns
2176
    -------
2177
    sys : `StateSpace`
2178
        Linear input/output system object with no states and only a direct
2179
        term that implements the summing junction.
2180

2181
    Examples
2182
    --------
2183
    >>> P = ct.tf(1, [1, 0], inputs='u', outputs='y')
2184
    >>> C = ct.tf(10, [1, 1], inputs='e', outputs='u')
2185
    >>> sumblk = ct.summing_junction(inputs=['r', '-y'], output='e')
2186
    >>> T = ct.interconnect([P, C, sumblk], inputs='r', outputs='y')
2187
    >>> T.ninputs, T.noutputs, T.nstates
2188
    (1, 1, 2)
2189

2190
    """
2191
    # Utility function to parse input and output signal lists
2192
    def _parse_list(signals, signame='input', prefix='u'):
9✔
2193
        # Parse signals, including gains
2194
        if isinstance(signals, int):
9✔
2195
            nsignals = signals
9✔
2196
            names = ["%s[%d]" % (prefix, i) for i in range(nsignals)]
9✔
2197
            gains = np.ones((nsignals,))
9✔
2198
        elif isinstance(signals, str):
9✔
2199
            nsignals = 1
×
2200
            gains = [-1 if signals[0] == '-' else 1]
×
2201
            names = [signals[1:] if signals[0] == '-' else signals]
×
2202
        elif isinstance(signals, list) and \
9✔
2203
             all([isinstance(x, str) for x in signals]):
2204
            nsignals = len(signals)
9✔
2205
            gains = np.ones((nsignals,))
9✔
2206
            names = []
9✔
2207
            for i in range(nsignals):
9✔
2208
                if signals[i][0] == '-':
9✔
2209
                    gains[i] = -1
9✔
2210
                    names.append(signals[i][1:])
9✔
2211
                else:
2212
                    names.append(signals[i])
9✔
2213
        else:
2214
            raise ValueError(
9✔
2215
                "could not parse %s description '%s'"
2216
                % (signame, str(signals)))
2217

2218
        # Return the parsed list
2219
        return nsignals, names, gains
9✔
2220

2221
    # Parse system and signal names (with some minor pre-processing)
2222
    if input is not None:
9✔
2223
        kwargs['inputs'] = inputs       # positional/keyword -> keyword
9✔
2224
    if output is not None:
9✔
2225
        kwargs['output'] = output       # positional/keyword -> keyword
9✔
2226
    name, inputs, output, states, dt = _process_iosys_keywords(
9✔
2227
        kwargs, {'inputs': None, 'outputs': 'y'}, end=True)
2228
    if inputs is None:
9✔
2229
        raise TypeError("input specification is required")
9✔
2230

2231
    # Read the input list
2232
    ninputs, input_names, input_gains = _parse_list(
9✔
2233
        inputs, signame="input", prefix=prefix)
2234
    noutputs, output_names, output_gains = _parse_list(
9✔
2235
        output, signame="output", prefix='y')
2236
    if noutputs > 1:
9✔
2237
        raise NotImplementedError("vector outputs not yet supported")
2238

2239
    # If the dimension keyword is present, vectorize inputs and outputs
2240
    if isinstance(dimension, int) and dimension >= 1:
9✔
2241
        # Create a new list of input/output names and update parameters
2242
        input_names = ["%s[%d]" % (name, dim)
9✔
2243
                       for name in input_names
2244
                       for dim in range(dimension)]
2245
        ninputs = ninputs * dimension
9✔
2246

2247
        output_names = ["%s[%d]" % (name, dim)
9✔
2248
                        for name in output_names
2249
                        for dim in range(dimension)]
2250
        noutputs = noutputs * dimension
9✔
2251
    elif dimension is not None:
9✔
2252
        raise ValueError(
9✔
2253
            "unrecognized dimension value '%s'" % str(dimension))
2254
    else:
2255
        dimension = 1
9✔
2256

2257
    # Create the direct term
2258
    D = np.kron(input_gains * output_gains[0], np.eye(dimension))
9✔
2259

2260
    # Create a linear system of the appropriate size
2261
    ss_sys = StateSpace(
9✔
2262
        np.zeros((0, 0)), np.ones((0, ninputs)), np.ones((noutputs, 0)), D)
2263

2264
    # Create a StateSpace
2265
    return StateSpace(
9✔
2266
        ss_sys, inputs=input_names, outputs=output_names, name=name)
2267

2268
#
2269
# Utility functions
2270
#
2271

2272
def _ssmatrix(data, axis=1, square=None, rows=None, cols=None, name=None):
9✔
2273
    """Convert argument to a (possibly empty) 2D state space matrix.
2274

2275
    This function can be used to process the matrices that define a
2276
    state-space system. The axis keyword argument makes it convenient
2277
    to specify that if the input is a vector, it is a row (axis=1) or
2278
    column (axis=0) vector.
2279

2280
    Parameters
2281
    ----------
2282
    data : array, list, or string
2283
        Input data defining the contents of the 2D array.
2284
    axis : 0 or 1
2285
        If input data is 1D, which axis to use for return object.  The
2286
        default is 1, corresponding to a row matrix.
2287
    square : bool, optional
2288
        If set to True, check that the input matrix is square.
2289
    rows : int, optional
2290
        If set, check that the input matrix has the given number of rows.
2291
    cols : int, optional
2292
        If set, check that the input matrix has the given number of columns.
2293
    name : str, optional
2294
        Name of the state-space matrix being checked (for error messages).
2295

2296
    Returns
2297
    -------
2298
    arr : 2D array, with shape (0, 0) if a is empty
2299

2300
    """
2301
    # Process the name of the object, if available
2302
    name = "" if name is None else " " + name
9✔
2303

2304
    # Convert the data into an array (always making a copy)
2305
    arr = np.array(data, dtype=float)
9✔
2306
    ndim = arr.ndim
9✔
2307
    shape = arr.shape
9✔
2308

2309
    # Change the shape of the array into a 2D array
2310
    if (ndim > 2):
9✔
2311
        raise ValueError(f"state-space matrix{name} must be 2-dimensional")
×
2312

2313
    elif (ndim == 2 and shape == (1, 0)) or \
9✔
2314
         (ndim == 1 and shape == (0, )):
2315
        # Passed an empty matrix or empty vector; change shape to (0, 0)
2316
        shape = (0, 0)
9✔
2317

2318
    elif ndim == 1:
9✔
2319
        # Passed a row or column vector
2320
        shape = (1, shape[0]) if axis == 1 else (shape[0], 1)
9✔
2321

2322
    elif ndim == 0:
9✔
2323
        # Passed a constant; turn into a matrix
2324
        shape = (1, 1)
9✔
2325

2326
    # Check to make sure any conditions are satisfied
2327
    if square and shape[0] != shape[1]:
9✔
2328
        raise ControlDimension(
9✔
2329
            f"state-space matrix{name} must be a square matrix")
2330

2331
    if rows is not None and shape[0] != rows:
9✔
2332
        raise ControlDimension(
9✔
2333
            f"state-space matrix{name} has the wrong number of rows; "
2334
            f"expected {rows} instead of {shape[0]}")
2335

2336
    if cols is not None and shape[1] != cols:
9✔
2337
        raise ControlDimension(
9✔
2338
            f"state-space matrix{name} has the wrong number of columns; "
2339
            f"expected {cols} instead of {shape[1]}")
2340

2341
    #  Create the actual object used to store the result
2342
    return arr.reshape(shape)
9✔
2343

2344

2345
def _f2s(f):
9✔
2346
    """Format floating point number f for StateSpace._repr_latex_.
2347

2348
    Numbers are converted to strings with statesp.latex_num_format.
2349

2350
    Inserts column separators, etc., as needed.
2351
    """
2352
    fmt = "{:" + config.defaults['statesp.latex_num_format'] + "}"
9✔
2353
    sraw = fmt.format(f)
9✔
2354
    # significant-exponent
2355
    se = sraw.lower().split('e')
9✔
2356
    # whole-fraction
2357
    wf = se[0].split('.')
9✔
2358
    s = wf[0]
9✔
2359
    if wf[1:]:
9✔
2360
        s += r'.&\hspace{{-1em}}{frac}'.format(frac=wf[1])
9✔
2361
    else:
2362
        s += r'\phantom{.}&\hspace{-1em}'
9✔
2363

2364
    if se[1:]:
9✔
2365
        s += r'&\hspace{{-1em}}\cdot10^{{{:d}}}'.format(int(se[1]))
9✔
2366
    else:
2367
        s += r'&\hspace{-1em}\phantom{\cdot}'
9✔
2368

2369
    return s
9✔
2370

2371

2372
def _convert_to_statespace(sys, use_prefix_suffix=False, method=None):
9✔
2373
    """Convert a system to state space form (if needed).
2374

2375
    If `sys` is already a state space object, then it is returned.  If
2376
    `sys` is a transfer function object, then it is converted to a state
2377
    space and returned.
2378

2379
    Note: no renaming of inputs and outputs is performed; this should be done
2380
    by the calling function.
2381

2382
    """
2383
    import itertools
9✔
2384

2385
    from .xferfcn import TransferFunction
9✔
2386
    from .delaylti import DelayLTI
9✔
2387

2388
    if isinstance(sys, StateSpace) or isinstance(sys, DelayLTI):
9✔
2389
        return sys
9✔
2390

2391
    elif isinstance(sys, TransferFunction):
9✔
2392
        # Make sure the transfer function is proper
2393
        if any([[len(num) for num in col] for col in sys.num] >
9✔
2394
               [[len(num) for num in col] for col in sys.den]):
2395
            raise ValueError("transfer function is non-proper; can't "
9✔
2396
                             "convert to StateSpace system")
2397

2398
        if method is None and slycot_check() or method == 'slycot':
9✔
2399
            if not slycot_check():
9✔
2400
                raise ValueError("method='slycot' requires slycot")
4✔
2401

2402
            from slycot import td04ad
5✔
2403

2404
            # Change the numerator and denominator arrays so that the transfer
2405
            # function matrix has a common denominator.
2406
            # matrices are also sized/padded to fit td04ad
2407
            num, den, denorder = sys.minreal()._common_den()
5✔
2408
            num, den, denorder = sys._common_den()
5✔
2409

2410
            # transfer function to state space conversion now should work!
2411
            ssout = td04ad('C', sys.ninputs, sys.noutputs,
5✔
2412
                           denorder, den, num, tol=0)
2413

2414
            states = ssout[0]
5✔
2415
            newsys = StateSpace(
5✔
2416
                ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
2417
                ssout[3][:sys.noutputs, :states], ssout[4], sys.dt)
2418

2419
        elif method in [None, 'scipy']:
9✔
2420
            # SciPy tf->ss can't handle MIMO, but SISO is OK
2421
            maxn = max(max(len(n) for n in nrow)
9✔
2422
                       for nrow in sys.num)
2423
            maxd = max(max(len(d) for d in drow)
9✔
2424
                       for drow in sys.den)
2425
            if 1 == maxn and 1 == maxd:
9✔
2426
                D = empty((sys.noutputs, sys.ninputs), dtype=float)
4✔
2427
                for i, j in itertools.product(range(sys.noutputs),
4✔
2428
                                              range(sys.ninputs)):
2429
                    D[i, j] = sys.num_array[i, j][0] / sys.den_array[i, j][0]
4✔
2430
                newsys = StateSpace([], [], [], D, sys.dt)
4✔
2431
            else:
2432
                if not issiso(sys):
9✔
2433
                    raise ControlMIMONotImplemented(
4✔
2434
                        "MIMO system conversion not supported without Slycot")
2435

2436
                A, B, C, D = \
9✔
2437
                    sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
2438
                newsys = StateSpace(A, B, C, D, sys.dt)
9✔
2439
        else:
2440
            raise ValueError(f"unknown {method=}")
×
2441

2442
        # Copy over the signal (and system) names
2443
        newsys._copy_names(
9✔
2444
            sys,
2445
            prefix_suffix_name='converted' if use_prefix_suffix else None)
2446
        return newsys
9✔
2447

2448
    elif isinstance(sys, FrequencyResponseData):
9✔
2449
        raise TypeError("Can't convert FRD to StateSpace system.")
×
2450

2451
    # If this is a matrix, try to create a constant feedthrough
2452
    try:
9✔
2453
        D = _ssmatrix(np.atleast_2d(sys), name="D")
9✔
2454
        return StateSpace([], [], [], D, dt=None)
9✔
2455

2456
    except Exception:
9✔
2457
        raise TypeError("Can't convert given type to StateSpace system.")
9✔
2458

2459

2460
def _rss_generate(
9✔
2461
        states, inputs, outputs, cdtype, strictly_proper=False, name=None):
2462
    """Generate a random state space.
2463

2464
    This does the actual random state space generation expected from rss and
2465
    drss.  cdtype is 'c' for continuous systems and 'd' for discrete systems.
2466

2467
    """
2468

2469
    # Probability of repeating a previous root.
2470
    pRepeat = 0.05
9✔
2471
    # Probability of choosing a real root.  Note that when choosing a complex
2472
    # root, the conjugate gets chosen as well.  So the expected proportion of
2473
    # real roots is pReal / (pReal + 2 * (1 - pReal)).
2474
    pReal = 0.6
9✔
2475
    # Probability that an element in B or C will not be masked out.
2476
    pBCmask = 0.8
9✔
2477
    # Probability that an element in D will not be masked out.
2478
    pDmask = 0.3
9✔
2479
    # Probability that D = 0.
2480
    pDzero = 0.5
9✔
2481

2482
    # Check for valid input arguments.
2483
    if states < 1 or states % 1:
9✔
2484
        raise ValueError("states must be a positive integer.  states = %g." %
9✔
2485
                         states)
2486
    if inputs < 1 or inputs % 1:
9✔
2487
        raise ValueError("inputs must be a positive integer.  inputs = %g." %
9✔
2488
                         inputs)
2489
    if outputs < 1 or outputs % 1:
9✔
2490
        raise ValueError("outputs must be a positive integer.  outputs = %g." %
9✔
2491
                         outputs)
2492
    if cdtype not in ['c', 'd']:
9✔
2493
        raise ValueError("cdtype must be `c` or `d`")
9✔
2494

2495
    # Make some poles for A.  Preallocate a complex array.
2496
    poles = zeros(states) + zeros(states) * 0.j
9✔
2497
    i = 0
9✔
2498

2499
    while i < states:
9✔
2500
        if rand() < pRepeat and i != 0 and i != states - 1:
9✔
2501
            # Small chance of copying poles, if we're not at the first or last
2502
            # element.
2503
            if poles[i-1].imag == 0:
9✔
2504
                # Copy previous real pole.
2505
                poles[i] = poles[i-1]
9✔
2506
                i += 1
9✔
2507
            else:
2508
                # Copy previous complex conjugate pair of poles.
2509
                poles[i:i+2] = poles[i-2:i]
9✔
2510
                i += 2
9✔
2511
        elif rand() < pReal or i == states - 1:
9✔
2512
            # No-oscillation pole.
2513
            if cdtype == 'c':
9✔
2514
                poles[i] = -exp(randn()) + 0.j
9✔
2515
            else:
2516
                poles[i] = 2. * rand() - 1.
9✔
2517
            i += 1
9✔
2518
        else:
2519
            # Complex conjugate pair of oscillating poles.
2520
            if cdtype == 'c':
9✔
2521
                poles[i] = complex(-exp(randn()), 3. * exp(randn()))
9✔
2522
            else:
2523
                mag = rand()
9✔
2524
                phase = 2. * math.pi * rand()
9✔
2525
                poles[i] = complex(mag * cos(phase), mag * sin(phase))
9✔
2526
            poles[i+1] = complex(poles[i].real, -poles[i].imag)
9✔
2527
            i += 2
9✔
2528

2529
    # Now put the poles in A as real blocks on the diagonal.
2530
    A = zeros((states, states))
9✔
2531
    i = 0
9✔
2532
    while i < states:
9✔
2533
        if poles[i].imag == 0:
9✔
2534
            A[i, i] = poles[i].real
9✔
2535
            i += 1
9✔
2536
        else:
2537
            A[i, i] = A[i+1, i+1] = poles[i].real
9✔
2538
            A[i, i+1] = poles[i].imag
9✔
2539
            A[i+1, i] = -poles[i].imag
9✔
2540
            i += 2
9✔
2541
    # Finally, apply a transformation so that A is not block-diagonal.
2542
    while True:
9✔
2543
        T = randn(states, states)
9✔
2544
        try:
9✔
2545
            A = solve(T, A) @ T  # A = T \ A @ T
9✔
2546
            break
9✔
2547
        except LinAlgError:
×
2548
            # In the unlikely event that T is rank-deficient, iterate again.
2549
            pass
×
2550

2551
    # Make the remaining matrices.
2552
    B = randn(states, inputs)
9✔
2553
    C = randn(outputs, states)
9✔
2554
    D = randn(outputs, inputs)
9✔
2555

2556
    # Make masks to zero out some of the elements.
2557
    while True:
9✔
2558
        Bmask = rand(states, inputs) < pBCmask
9✔
2559
        if any(Bmask):  # Retry if we get all zeros.
9✔
2560
            break
9✔
2561
    while True:
9✔
2562
        Cmask = rand(outputs, states) < pBCmask
9✔
2563
        if any(Cmask):  # Retry if we get all zeros.
9✔
2564
            break
9✔
2565
    if rand() < pDzero:
9✔
2566
        Dmask = zeros((outputs, inputs))
9✔
2567
    else:
2568
        Dmask = rand(outputs, inputs) < pDmask
9✔
2569

2570
    # Apply masks.
2571
    B = B * Bmask
9✔
2572
    C = C * Cmask
9✔
2573
    D = D * Dmask if not strictly_proper else zeros(D.shape)
9✔
2574

2575
    if cdtype == 'c':
9✔
2576
        ss_args = (A, B, C, D)
9✔
2577
    else:
2578
        ss_args = (A, B, C, D, True)
9✔
2579
    return StateSpace(*ss_args, name=name)
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc