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

python-control / python-control / 12266904606

11 Dec 2024 12:03AM UTC coverage: 94.73% (+0.009%) from 94.721%
12266904606

Pull #1078

github

web-flow
Merge 0b9be7a84 into e2c0ff85f
Pull Request #1078: fix issue with multiplying MIMO LTI system by scalar

9330 of 9849 relevant lines covered (94.73%)

8.27 hits per line

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

96.82
control/statesp.py
1
"""statesp.py
2

3
State space representation and functions.
4

5
This file contains the StateSpace class, which is used to represent linear
6
systems in state space.  This is the primary representation for the
7
python-control library.
8

9
"""
10

11
"""Copyright (c) 2010 by California Institute of Technology
9✔
12
All rights reserved.
13

14
Redistribution and use in source and binary forms, with or without
15
modification, are permitted provided that the following conditions
16
are met:
17

18
1. Redistributions of source code must retain the above copyright
19
   notice, this list of conditions and the following disclaimer.
20

21
2. Redistributions in binary form must reproduce the above copyright
22
   notice, this list of conditions and the following disclaimer in the
23
   documentation and/or other materials provided with the distribution.
24

25
3. Neither the name of the California Institute of Technology nor
26
   the names of its contributors may be used to endorse or promote
27
   products derived from this software without specific prior
28
   written permission.
29

30
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
31
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
32
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
33
FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH
34
OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
36
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
37
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
38
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
39
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
40
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
41
SUCH DAMAGE.
42

43
Author: Richard M. Murray
44
Date: 24 May 09
45
Revised: Kevin K. Chen, Dec 10
46

47
$Id$
48
"""
49

50
import math
9✔
51
from collections.abc import Iterable
9✔
52
from copy import deepcopy
9✔
53
from warnings import warn
9✔
54

55
import numpy as np
9✔
56
import scipy as sp
9✔
57
import scipy.linalg
9✔
58
from numpy import any, asarray, concatenate, cos, delete, empty, exp, eye, \
9✔
59
    isinf, ones, pad, sin, squeeze, zeros
60
from numpy.linalg import LinAlgError, eigvals, matrix_rank, solve
9✔
61
from numpy.random import rand, randn
9✔
62
from scipy.signal import StateSpace as signalStateSpace
9✔
63
from scipy.signal import cont2discrete
9✔
64

65
from . import config
9✔
66
from .exception import ControlMIMONotImplemented, ControlSlycot, slycot_check
9✔
67
from .frdata import FrequencyResponseData
9✔
68
from .iosys import InputOutputSystem, NamedSignal, _process_dt_keyword, \
9✔
69
    _process_iosys_keywords, _process_signal_list, _process_subsys_index, \
70
    common_timebase, isdtime, issiso
71
from .lti import LTI, _process_frequency_response
9✔
72
from .nlsys import InterconnectedSystem, NonlinearIOSystem
9✔
73

74
try:
9✔
75
    from slycot import ab13dd
9✔
76
except ImportError:
4✔
77
    ab13dd = None
4✔
78

79
__all__ = ['StateSpace', 'LinearICSystem', 'ss2io', 'tf2io', 'tf2ss', 'ssdata',
9✔
80
           'linfnorm', 'ss', 'rss', 'drss', 'summing_junction']
81

82
# Define module default parameter values
83
_statesp_defaults = {
9✔
84
    'statesp.remove_useless_states': False,
85
    'statesp.latex_num_format': '.3g',
86
    'statesp.latex_repr_type': 'partitioned',
87
    'statesp.latex_maxsize': 10,
88
    }
89

90

91
class StateSpace(NonlinearIOSystem, LTI):
9✔
92
    r"""StateSpace(A, B, C, D[, dt])
93

94
    A class for representing state-space models.
95

96
    The StateSpace class is used to represent state-space realizations of
97
    linear time-invariant (LTI) systems:
98

99
    .. math::
100

101
          dx/dt &= A x + B u \\
102
              y &= C x + D u
103

104
    where `u` is the input, `y` is the output, and `x` is the state.
105

106
    Parameters
107
    ----------
108
    A, B, C, D: array_like
109
        System matrices of the appropriate dimensions.
110
    dt : None, True or float, optional
111
        System timebase. 0 (default) indicates continuous
112
        time, True indicates discrete time with unspecified sampling
113
        time, positive number is discrete time with specified
114
        sampling time, None indicates unspecified timebase (either
115
        continuous or discrete time).
116

117
    Attributes
118
    ----------
119
    ninputs, noutputs, nstates : int
120
        Number of input, output and state variables.
121
    A, B, C, D : 2D arrays
122
        System matrices defining the input/output dynamics.
123
    dt : None, True or float
124
        System timebase. 0 (default) indicates continuous time, True indicates
125
        discrete time with unspecified sampling time, positive number is
126
        discrete time with specified sampling time, None indicates unspecified
127
        timebase (either continuous or discrete time).
128

129
    Notes
130
    -----
131
    The main data members in the ``StateSpace`` class are the A, B, C, and D
132
    matrices.  The class also keeps track of the number of states (i.e.,
133
    the size of A).
134

135
    A discrete time system is created by specifying a nonzero 'timebase', dt
136
    when the system is constructed:
137

138
    * dt = 0: continuous time system (default)
139
    * dt > 0: discrete time system with sampling period 'dt'
140
    * dt = True: discrete time with unspecified sampling period
141
    * dt = None: no timebase specified
142

143
    Systems must have compatible timebases in order to be combined. A discrete
144
    time system with unspecified sampling time (`dt = True`) can be combined
145
    with a system having a specified sampling time; the result will be a
146
    discrete time system with the sample time of the latter system. Similarly,
147
    a system with timebase `None` can be combined with a system having any
148
    timebase; the result will have the timebase of the latter system.
149
    The default value of dt can be changed by changing the value of
150
    ``control.config.defaults['control.default_dt']``.
151

152
    A state space system is callable and returns the value of the transfer
153
    function evaluated at a point in the complex plane.  See
154
    :meth:`~control.StateSpace.__call__` for a more detailed description.
155

156
    Subsystems corresponding to selected input/output pairs can be
157
    created by indexing the state space system::
158

159
        subsys = sys[output_spec, input_spec]
160

161
    The input and output specifications can be single integers, lists of
162
    integers, or slices.  In addition, the strings representing the names
163
    of the signals can be used and will be replaced with the equivalent
164
    signal offsets.  The subsystem is created by truncating the inputs and
165
    outputs, but leaving the full set of system states.
166

167
    StateSpace instances have support for IPython LaTeX output,
168
    intended for pretty-printing in Jupyter notebooks.  The LaTeX
169
    output can be configured using
170
    `control.config.defaults['statesp.latex_num_format']` and
171
    `control.config.defaults['statesp.latex_repr_type']`.  The LaTeX output is
172
    tailored for MathJax, as used in Jupyter, and may look odd when
173
    typeset by non-MathJax LaTeX systems.
174

175
    `control.config.defaults['statesp.latex_num_format']` is a format string
176
    fragment, specifically the part of the format string after `'{:'`
177
    used to convert floating-point numbers to strings.  By default it
178
    is `'.3g'`.
179

180
    `control.config.defaults['statesp.latex_repr_type']` must either be
181
    `'partitioned'` or `'separate'`.  If `'partitioned'`, the A, B, C, D
182
    matrices are shown as a single, partitioned matrix; if
183
    `'separate'`, the matrices are shown separately.
184

185
    """
186
    def __init__(self, *args, **kwargs):
9✔
187
        """StateSpace(A, B, C, D[, dt])
188

189
        Construct a state space object.
190

191
        The default constructor is StateSpace(A, B, C, D), where A, B, C, D
192
        are matrices or equivalent objects.  To create a discrete time system,
193
        use StateSpace(A, B, C, D, dt) where `dt` is the sampling time (or
194
        True for unspecified sampling time).  To call the copy constructor,
195
        call StateSpace(sys), where sys is a StateSpace object.
196

197
        The `remove_useless_states` keyword can be used to scan the A, B, and
198
        C matrices for rows or columns of zeros.  If the zeros are such that a
199
        particular state has no effect on the input-output dynamics, then that
200
        state is removed from the A, B, and C matrices.  If not specified, the
201
        value is read from `config.defaults['statesp.remove_useless_states']`
202
        (default = False).
203

204
        """
205
        #
206
        # Process positional arguments
207
        #
208

209
        if len(args) == 4:
9✔
210
            # The user provided A, B, C, and D matrices.
211
            A, B, C, D = args
9✔
212

213
        elif len(args) == 5:
9✔
214
            # Discrete time system
215
            A, B, C, D, dt = args
9✔
216
            if 'dt' in kwargs:
9✔
217
                warn("received multiple dt arguments, "
9✔
218
                     "using positional arg dt = %s" % dt)
219
            kwargs['dt'] = dt
9✔
220
            args = args[:-1]
9✔
221

222
        elif len(args) == 1:
9✔
223
            # Use the copy constructor
224
            if not isinstance(args[0], StateSpace):
9✔
225
                raise TypeError(
9✔
226
                    "the one-argument constructor can only take in a "
227
                    "StateSpace object; received %s" % type(args[0]))
228
            A = args[0].A
9✔
229
            B = args[0].B
9✔
230
            C = args[0].C
9✔
231
            D = args[0].D
9✔
232
            if 'dt' not in kwargs:
9✔
233
                kwargs['dt'] = args[0].dt
9✔
234

235
        else:
236
            raise TypeError(
9✔
237
                "Expected 1, 4, or 5 arguments; received %i." % len(args))
238

239
        # Convert all matrices to standard form
240
        A = _ssmatrix(A)
9✔
241
        # if B is a 1D array, turn it into a column vector if it fits
242
        if np.asarray(B).ndim == 1 and len(B) == A.shape[0]:
9✔
243
            B = _ssmatrix(B, axis=0)
9✔
244
        else:
245
            B = _ssmatrix(B)
9✔
246
        if np.asarray(C).ndim == 1 and len(C) == A.shape[0]:
9✔
247
            C = _ssmatrix(C, axis=1)
9✔
248
        else:
249
            C = _ssmatrix(C, axis=0)    # if this doesn't work, error below
9✔
250
        if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0:
9✔
251
            # If D is a scalar zero, broadcast it to the proper size
252
            D = np.zeros((C.shape[0], B.shape[1]))
9✔
253
        D = _ssmatrix(D)
9✔
254

255
        # If only direct term is present, adjust sizes of C and D if needed
256
        if D.size > 0 and B.size == 0:
9✔
257
            B = np.zeros((0, D.shape[1]))
9✔
258
        if D.size > 0 and C.size == 0:
9✔
259
            C = np.zeros((D.shape[0], 0))
9✔
260

261
        # Matrices defining the linear system
262
        self.A = A
9✔
263
        self.B = B
9✔
264
        self.C = C
9✔
265
        self.D = D
9✔
266

267
        #
268
        # Process keyword arguments
269
        #
270

271
        remove_useless_states = kwargs.pop(
9✔
272
            'remove_useless_states',
273
            config.defaults['statesp.remove_useless_states'])
274

275
        # Process iosys keywords
276
        defaults = args[0] if len(args) == 1 else \
9✔
277
            {'inputs': B.shape[1], 'outputs': C.shape[0],
278
             'states': A.shape[0]}
279
        name, inputs, outputs, states, dt = _process_iosys_keywords(
9✔
280
            kwargs, defaults, static=(A.size == 0))
281

282
        # Create updfcn and outfcn
283
        updfcn = lambda t, x, u, params: \
9✔
284
            self.A @ np.atleast_1d(x) + self.B @ np.atleast_1d(u)
285
        outfcn = lambda t, x, u, params: \
9✔
286
            self.C @ np.atleast_1d(x) + self.D @ np.atleast_1d(u)
287

288
        # Initialize NonlinearIOSystem object
289
        super().__init__(
9✔
290
            updfcn, outfcn,
291
            name=name, inputs=inputs, outputs=outputs,
292
            states=states, dt=dt, **kwargs)
293

294
        # Reset shapes if the system is static
295
        if self._isstatic():
9✔
296
            A.shape = (0, 0)
9✔
297
            B.shape = (0, self.ninputs)
9✔
298
            C.shape = (self.noutputs, 0)
9✔
299

300
        #
301
        # Check to make sure everything is consistent
302
        #
303
        # Check that the matrix sizes are consistent
304
        def _check_shape(matrix, expected, name):
9✔
305
            if matrix.shape != expected:
9✔
306
                raise ValueError(
9✔
307
                    f"{name} is the wrong shape; "
308
                    f"expected {expected} instead of {matrix.shape}")
309
        _check_shape(A, (self.nstates, self.nstates), "A")
9✔
310
        _check_shape(B, (self.nstates, self.ninputs), "B")
9✔
311
        _check_shape(C, (self.noutputs, self.nstates), "C")
9✔
312
        _check_shape(D, (self.noutputs, self.ninputs), "D")
9✔
313

314
        #
315
        # Final processing
316
        #
317
        # Check for states that don't do anything, and remove them
318
        if remove_useless_states:
9✔
319
            self._remove_useless_states()
9✔
320

321
    #
322
    # Class attributes
323
    #
324
    # These attributes are defined as class attributes so that they are
325
    # documented properly.  They are "overwritten" in __init__.
326
    #
327

328
    #: Number of system inputs.
329
    #:
330
    #: :meta hide-value:
331
    ninputs = 0
9✔
332

333
    #: Number of system outputs.
334
    #:
335
    #: :meta hide-value:
336
    noutputs = 0
9✔
337

338
    #: Number of system states.
339
    #:
340
    #: :meta hide-value:
341
    nstates = 0
9✔
342

343
    #: Dynamics matrix.
344
    #:
345
    #: :meta hide-value:
346
    A = []
9✔
347

348
    #: Input matrix.
349
    #:
350
    #: :meta hide-value:
351
    B = []
9✔
352

353
    #: Output matrix.
354
    #:
355
    #: :meta hide-value:
356
    C = []
9✔
357

358
    #: Direct term.
359
    #:
360
    #: :meta hide-value:
361
    D = []
9✔
362

363
    #
364
    # Getter and setter functions for legacy state attributes
365
    #
366
    # For this iteration, generate a deprecation warning whenever the
367
    # getter/setter is called.  For a future iteration, turn it into a
368
    # future warning, so that users will see it.
369
    #
370

371
    def _get_states(self):
9✔
372
        warn("The StateSpace `states` attribute will be deprecated in a "
×
373
             "future release.  Use `nstates` instead.",
374
             FutureWarning, stacklevel=2)
375
        return self.nstates
×
376

377
    def _set_states(self, value):
9✔
378
        warn("The StateSpace `states` attribute will be deprecated in a "
×
379
             "future release.  Use `nstates` instead.",
380
             FutureWarning, stacklevel=2)
381
        self.nstates = value
×
382

383
    #: Deprecated attribute; use :attr:`nstates` instead.
384
    #:
385
    #: The ``state`` attribute was used to store the number of states for : a
386
    #: state space system.  It is no longer used.  If you need to access the
387
    #: number of states, use :attr:`nstates`.
388
    states = property(_get_states, _set_states)
9✔
389

390
    def _remove_useless_states(self):
9✔
391
        """Check for states that don't do anything, and remove them.
392

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

397
        """
398

399
        # Search for useless states and get indices of these states.
400
        ax1_A = np.where(~self.A.any(axis=1))[0]
9✔
401
        ax1_B = np.where(~self.B.any(axis=1))[0]
9✔
402
        ax0_A = np.where(~self.A.any(axis=0))[-1]
9✔
403
        ax0_C = np.where(~self.C.any(axis=0))[-1]
9✔
404
        useless_1 = np.intersect1d(ax1_A, ax1_B, assume_unique=True)
9✔
405
        useless_2 = np.intersect1d(ax0_A, ax0_C, assume_unique=True)
9✔
406
        useless = np.union1d(useless_1, useless_2)
9✔
407

408
        # Remove the useless states.
409
        self.A = delete(self.A, useless, 0)
9✔
410
        self.A = delete(self.A, useless, 1)
9✔
411
        self.B = delete(self.B, useless, 0)
9✔
412
        self.C = delete(self.C, useless, 1)
9✔
413

414
        # Remove any state names that we don't need
415
        self.set_states(
9✔
416
            [self.state_labels[i] for i in range(self.nstates)
417
             if i not in useless])
418

419
    def __str__(self):
9✔
420
        """Return string representation of the state space system."""
421
        string = f"{InputOutputSystem.__str__(self)}\n\n"
9✔
422
        string += "\n".join([
9✔
423
            "{} = {}\n".format(Mvar,
424
                               "\n    ".join(str(M).splitlines()))
425
            for Mvar, M in zip(["A", "B", "C", "D"],
426
                               [self.A, self.B, self.C, self.D])])
427
        if self.isdtime(strict=True):
9✔
428
            string += f"\ndt = {self.dt}\n"
9✔
429
        return string
9✔
430

431
    # represent to implement a re-loadable version
432
    def __repr__(self):
9✔
433
        """Print state-space system in loadable form."""
434
        # TODO: add input/output names (?)
435
        return "StateSpace({A}, {B}, {C}, {D}{dt})".format(
9✔
436
            A=self.A.__repr__(), B=self.B.__repr__(),
437
            C=self.C.__repr__(), D=self.D.__repr__(),
438
            dt=(isdtime(self, strict=True) and ", {}".format(self.dt)) or '')
439

440
    def _latex_partitioned_stateless(self):
9✔
441
        """`Partitioned` matrix LaTeX representation for stateless systems
442

443
        Model is presented as a matrix, D.  No partition lines are shown.
444

445
        Returns
446
        -------
447
        s : string with LaTeX representation of model
448
        """
449
        lines = [
9✔
450
            r'$$',
451
            (r'\left('
452
             + r'\begin{array}'
453
             + r'{' + 'rll' * self.ninputs + '}')
454
            ]
455

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

460
        lines.extend([
9✔
461
            r'\end{array}'
462
            r'\right)'
463
            + self._latex_dt(),
464
            r'$$'])
465

466
        return '\n'.join(lines)
9✔
467

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

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

474
        Returns
475
        -------
476
        s : string with LaTeX representation of model
477
        """
478
        if self.nstates == 0:
9✔
479
            return self._latex_partitioned_stateless()
9✔
480

481
        lines = [
9✔
482
            r'$$',
483
            (r'\left('
484
             + r'\begin{array}'
485
             + r'{' + 'rll' * self.nstates + '|' + 'rll' * self.ninputs + '}')
486
            ]
487

488
        for Ai, Bi in zip(asarray(self.A), asarray(self.B)):
9✔
489
            lines.append('&'.join([_f2s(Aij) for Aij in Ai]
9✔
490
                                  + [_f2s(Bij) for Bij in Bi])
491
                         + '\\\\')
492
        lines.append(r'\hline')
9✔
493
        for Ci, Di in zip(asarray(self.C), asarray(self.D)):
9✔
494
            lines.append('&'.join([_f2s(Cij) for Cij in Ci]
9✔
495
                                  + [_f2s(Dij) for Dij in Di])
496
                         + '\\\\')
497

498
        lines.extend([
9✔
499
            r'\end{array}'
500
            + r'\right)'
501
            + self._latex_dt(),
502
            r'$$'])
503

504
        return '\n'.join(lines)
9✔
505

506
    def _latex_separate(self):
9✔
507
        """Separate matrices LaTeX representation of state-space model
508

509
        Model is presented as separate, named, A, B, C, and D matrices.
510

511
        Returns
512
        -------
513
        s : string with LaTeX representation of model
514
        """
515
        lines = [
9✔
516
            r'$$',
517
            r'\begin{array}{ll}',
518
            ]
519

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

533
        if self.nstates > 0:
9✔
534
            lines.extend(fmt_matrix(self.A, 'A'))
9✔
535
            lines.append('&')
9✔
536
            lines.extend(fmt_matrix(self.B, 'B'))
9✔
537
            lines.append('\\\\')
9✔
538

539
            lines.extend(fmt_matrix(self.C, 'C'))
9✔
540
            lines.append('&')
9✔
541
        lines.extend(fmt_matrix(self.D, 'D'))
9✔
542

543
        lines.extend([
9✔
544
            r'\end{array}'
545
            + self._latex_dt(),
546
            r'$$'])
547

548
        return '\n'.join(lines)
9✔
549

550
    def _latex_dt(self):
9✔
551
        if self.isdtime(strict=True):
9✔
552
            if self.dt is True:
9✔
553
                return r"~,~dt=~\mathrm{True}"
9✔
554
            else:
555
                fmt = config.defaults['statesp.latex_num_format']
9✔
556
                return f"~,~dt={self.dt:{fmt}}"
9✔
557
        return ""
9✔
558

559
    def _repr_latex_(self):
9✔
560
        """LaTeX representation of state-space model
561

562
        Output is controlled by config options statesp.latex_repr_type,
563
        statesp.latex_num_format, and statesp.latex_maxsize.
564

565
        The output is primarily intended for Jupyter notebooks, which
566
        use MathJax to render the LaTeX, and the results may look odd
567
        when processed by a 'conventional' LaTeX system.
568

569

570
        Returns
571
        -------
572

573
        s : string with LaTeX representation of model, or None if
574
            either matrix dimension is greater than
575
            statesp.latex_maxsize
576

577
        """
578
        syssize = self.nstates + max(self.noutputs, self.ninputs)
9✔
579
        if syssize > config.defaults['statesp.latex_maxsize']:
9✔
580
            return None
9✔
581
        elif config.defaults['statesp.latex_repr_type'] == 'partitioned':
9✔
582
            return self._latex_partitioned()
9✔
583
        elif config.defaults['statesp.latex_repr_type'] == 'separate':
9✔
584
            return self._latex_separate()
9✔
585
        else:
586
            raise ValueError(
×
587
                "Unknown statesp.latex_repr_type '{cfg}'".format(
588
                    cfg=config.defaults['statesp.latex_repr_type']))
589

590
    # Negation of a system
591
    def __neg__(self):
9✔
592
        """Negate a state space system."""
593
        return StateSpace(self.A, self.B, -self.C, -self.D, self.dt)
9✔
594

595
    # Addition of two state space systems (parallel interconnection)
596
    def __add__(self, other):
9✔
597
        """Add two LTI systems (parallel connection)."""
598
        from .xferfcn import TransferFunction
9✔
599

600
        # Convert transfer functions to state space
601
        if isinstance(other, TransferFunction):
9✔
602
            # Convert the other argument to state space
603
            other = _convert_to_statespace(other)
9✔
604

605
        # Check for a couple of special cases
606
        if isinstance(other, (int, float, complex, np.number)):
9✔
607
            # Just adding a scalar; put it in the D matrix
608
            A, B, C = self.A, self.B, self.C
9✔
609
            D = self.D + other
9✔
610
            dt = self.dt
9✔
611

612
        elif isinstance(other, np.ndarray):
9✔
613
            other = np.atleast_2d(other)
9✔
614
            if self.ninputs != other.shape[0]:
9✔
615
                raise ValueError("array has incompatible shape")
9✔
616
            A, B, C = self.A, self.B, self.C
9✔
617
            D = self.D + other
9✔
618
            dt = self.dt
9✔
619

620
        elif not isinstance(other, StateSpace):
9✔
621
            return NotImplemented       # let other.__rmul__ handle it
9✔
622

623
        else:
624
            # Check to make sure the dimensions are OK
625
            if ((self.ninputs != other.ninputs) or
9✔
626
                    (self.noutputs != other.noutputs)):
627
                raise ValueError(
9✔
628
                    "can't add systems with incompatible inputs and outputs")
629

630
            dt = common_timebase(self.dt, other.dt)
9✔
631

632
            # Concatenate the various arrays
633
            A = concatenate((
9✔
634
                concatenate((self.A, zeros((self.A.shape[0],
635
                                            other.A.shape[-1]))), axis=1),
636
                concatenate((zeros((other.A.shape[0], self.A.shape[-1])),
637
                             other.A), axis=1)), axis=0)
638
            B = concatenate((self.B, other.B), axis=0)
9✔
639
            C = concatenate((self.C, other.C), axis=1)
9✔
640
            D = self.D + other.D
9✔
641

642
        return StateSpace(A, B, C, D, dt)
9✔
643

644
    # Right addition - just switch the arguments
645
    def __radd__(self, other):
9✔
646
        """Right add two LTI systems (parallel connection)."""
647
        return self + other
9✔
648

649
    # Subtraction of two state space systems (parallel interconnection)
650
    def __sub__(self, other):
9✔
651
        """Subtract two LTI systems."""
652
        return self + (-other)
9✔
653

654
    def __rsub__(self, other):
9✔
655
        """Right subtract two LTI systems."""
656
        return other + (-self)
9✔
657

658
    # Multiplication of two state space systems (series interconnection)
659
    def __mul__(self, other):
9✔
660
        """Multiply two LTI objects (serial connection)."""
661
        from .xferfcn import TransferFunction
9✔
662

663
        # Convert transfer functions to state space
664
        if isinstance(other, TransferFunction):
9✔
665
            # Convert the other argument to state space
666
            other = _convert_to_statespace(other)
9✔
667

668
        # Check for a couple of special cases
669
        if isinstance(other, (int, float, complex, np.number)):
9✔
670
            # Just multiplying by a scalar; change the output
671
            A, C = self.A, self.C
9✔
672
            B = self.B * other
9✔
673
            D = self.D * other
9✔
674
            dt = self.dt
9✔
675

676
        elif isinstance(other, np.ndarray):
9✔
677
            other = np.atleast_2d(other)
9✔
678
            if self.ninputs != other.shape[0]:
9✔
679
                raise ValueError("array has incompatible shape")
9✔
680
            A, C = self.A, self.C
9✔
681
            B = self.B @ other
9✔
682
            D = self.D @ other
9✔
683
            dt = self.dt
9✔
684

685
        elif not isinstance(other, StateSpace):
9✔
686
            return NotImplemented       # let other.__rmul__ handle it
9✔
687

688
        else:
689
            # Check to make sure the dimensions are OK
690
            if self.ninputs != other.noutputs:
9✔
691
                raise ValueError(
9✔
692
                    "can't multiply systems with incompatible"
693
                    " inputs and outputs")
694
            dt = common_timebase(self.dt, other.dt)
9✔
695

696
            # Concatenate the various arrays
697
            A = concatenate(
9✔
698
                (concatenate((other.A,
699
                              zeros((other.A.shape[0], self.A.shape[1]))),
700
                             axis=1),
701
                 concatenate((self.B @ other.C, self.A), axis=1)),
702
                axis=0)
703
            B = concatenate((other.B, self.B @ other.D), axis=0)
9✔
704
            C = concatenate((self.D @ other.C, self.C), axis=1)
9✔
705
            D = self.D @ other.D
9✔
706

707
        return StateSpace(A, B, C, D, dt)
9✔
708

709
    # Right multiplication of two state space systems (series interconnection)
710
    # Just need to convert LH argument to a state space object
711
    def __rmul__(self, other):
9✔
712
        """Right multiply two LTI objects (serial connection)."""
713
        from .xferfcn import TransferFunction
9✔
714

715
        # Convert transfer functions to state space
716
        if isinstance(other, TransferFunction):
9✔
717
            # Convert the other argument to state space
718
            other = _convert_to_statespace(other)
×
719

720
        # Check for a couple of special cases
721
        if isinstance(other, (int, float, complex, np.number)):
9✔
722
            # Just multiplying by a scalar; change the input
723
            B = other * self.B
9✔
724
            D = other * self.D
9✔
725
            return StateSpace(self.A, B, self.C, D, self.dt)
9✔
726

727
        elif isinstance(other, np.ndarray):
9✔
728
            C = np.atleast_2d(other) @ self.C
9✔
729
            D = np.atleast_2d(other) @ self.D
9✔
730
            return StateSpace(self.A, self.B, C, D, self.dt)
9✔
731

732
        if not isinstance(other, StateSpace):
9✔
733
            return NotImplemented
9✔
734

735
        return other * self
9✔
736

737
    # TODO: general __truediv__ requires descriptor system support
738
    def __truediv__(self, other):
9✔
739
        """Division of state space systems by TFs, FRDs, scalars, and arrays"""
740
        if not isinstance(other, (LTI, InputOutputSystem)):
9✔
741
            return self * (1/other)
9✔
742
        else:
743
            return NotImplemented
9✔
744

745
    def __call__(self, x, squeeze=None, warn_infinite=True):
9✔
746
        """Evaluate system's frequency response at complex frequencies.
747

748
        Returns the complex frequency response `sys(x)` where `x` is `s` for
749
        continuous-time systems and `z` for discrete-time systems.
750

751
        To evaluate at a frequency omega in radians per second, enter
752
        ``x = omega * 1j``, for continuous-time systems, or
753
        ``x = exp(1j * omega * dt)`` for discrete-time systems. Or use
754
        :meth:`StateSpace.frequency_response`.
755

756
        Parameters
757
        ----------
758
        x : complex or complex 1D array_like
759
            Complex frequencies
760
        squeeze : bool, optional
761
            If squeeze=True, remove single-dimensional entries from the shape
762
            of the output even if the system is not SISO. If squeeze=False,
763
            keep all indices (output, input and, if omega is array_like,
764
            frequency) even if the system is SISO. The default value can be
765
            set using config.defaults['control.squeeze_frequency_response'].
766
        warn_infinite : bool, optional
767
            If set to `False`, don't warn if frequency response is infinite.
768

769
        Returns
770
        -------
771
        fresp : complex ndarray
772
            The frequency response of the system.  If the system is SISO and
773
            squeeze is not True, the shape of the array matches the shape of
774
            omega.  If the system is not SISO or squeeze is False, the first
775
            two dimensions of the array are indices for the output and input
776
            and the remaining dimensions match omega.  If ``squeeze`` is True
777
            then single-dimensional axes are removed.
778

779
        """
780
        # Use Slycot if available
781
        out = self.horner(x, warn_infinite=warn_infinite)
9✔
782
        return _process_frequency_response(self, x, out, squeeze=squeeze)
9✔
783

784
    def slycot_laub(self, x):
9✔
785
        """Evaluate system's transfer function at complex frequency
786
        using Laub's method from Slycot.
787

788
        Expects inputs and outputs to be formatted correctly. Use ``sys(x)``
789
        for a more user-friendly interface.
790

791
        Parameters
792
        ----------
793
        x : complex array_like or complex
794
            Complex frequency
795

796
        Returns
797
        -------
798
        output : (number_outputs, number_inputs, len(x)) complex ndarray
799
            Frequency response
800
        """
801
        from slycot import tb05ad
9✔
802

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

806
        # Make sure that we are operating on a simple list
807
        if len(x_arr.shape) > 1:
5✔
808
            raise ValueError("input list must be 1D")
5✔
809

810
        # preallocate
811
        n = self.nstates
5✔
812
        m = self.ninputs
5✔
813
        p = self.noutputs
5✔
814
        out = np.empty((p, m, len(x_arr)), dtype=complex)
5✔
815
        # The first call both evaluates C(sI-A)^-1 B and also returns
816
        # Hessenberg transformed matrices at, bt, ct.
817
        result = tb05ad(n, m, p, x_arr[0], self.A, self.B, self.C, job='NG')
5✔
818
        # When job='NG', result = (at, bt, ct, g_i, hinvb, info)
819
        at = result[0]
5✔
820
        bt = result[1]
5✔
821
        ct = result[2]
5✔
822

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

826
        # Now, iterate through the remaining frequencies using the
827
        # transformed state matrices, at, bt, ct.
828

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

834
            # kk+1 because enumerate starts at kk = 0.
835
            # but zero-th spot is already filled.
836
            out[:, :, kk+1] = result[0] + self.D
5✔
837
        return out
5✔
838

839
    def horner(self, x, warn_infinite=True):
9✔
840
        """Evaluate system's transfer function at complex frequency
841
        using Laub's or Horner's method.
842

843
        Evaluates `sys(x)` where `x` is `s` for continuous-time systems and `z`
844
        for discrete-time systems.
845

846
        Expects inputs and outputs to be formatted correctly. Use ``sys(x)``
847
        for a more user-friendly interface.
848

849
        Parameters
850
        ----------
851
        x : complex array_like or complex
852
            Complex frequencies
853

854
        Returns
855
        -------
856
        output : (self.noutputs, self.ninputs, len(x)) complex ndarray
857
            Frequency response
858

859
        Notes
860
        -----
861
        Attempts to use Laub's method from Slycot library, with a
862
        fall-back to python code.
863
        """
864
        # Make sure the argument is a 1D array of complex numbers
865
        x_arr = np.atleast_1d(x).astype(complex, copy=False)
9✔
866

867
        # return fast on systems with 0 or 1 state
868
        if self.nstates == 0:
9✔
869
            return self.D[:, :, np.newaxis] \
9✔
870
                * np.ones_like(x_arr, dtype=complex)
871
        elif self.nstates == 1:
9✔
872
            with np.errstate(divide='ignore', invalid='ignore'):
9✔
873
                out = self.C[:, :, np.newaxis] \
9✔
874
                    / (x_arr - self.A[0, 0]) \
875
                    * self.B[:, :, np.newaxis] \
876
                    + self.D[:, :, np.newaxis]
877
            out[np.isnan(out)] = complex(np.inf, np.nan)
9✔
878
            return out
9✔
879

880
        try:
9✔
881
            out = self.slycot_laub(x_arr)
9✔
882
        except (ImportError, Exception):
9✔
883
            # Fall back because either Slycot unavailable or cannot handle
884
            # certain cases.
885

886
            # Make sure that we are operating on a simple list
887
            if len(x_arr.shape) > 1:
9✔
888
                raise ValueError("input list must be 1D")
9✔
889

890
            # Preallocate
891
            out = empty((self.noutputs, self.ninputs, len(x_arr)),
9✔
892
                        dtype=complex)
893

894
            # TODO: can this be vectorized?
895
            for idx, x_idx in enumerate(x_arr):
9✔
896
                try:
9✔
897
                    xr = solve(x_idx * eye(self.nstates) - self.A, self.B)
9✔
898
                    out[:, :, idx] = self.C @ xr + self.D
9✔
899
                except LinAlgError:
9✔
900
                    # Issue a warning messsage, for consistency with xferfcn
901
                    if warn_infinite:
9✔
902
                        warn("singular matrix in frequency response",
9✔
903
                             RuntimeWarning)
904

905
                    # Evaluating at a pole.  Return value depends if there
906
                    # is a zero at the same point or not.
907
                    if x_idx in self.zeros():
9✔
908
                        out[:, :, idx] = complex(np.nan, np.nan)
9✔
909
                    else:
910
                        out[:, :, idx] = complex(np.inf, np.nan)
9✔
911

912
        return out
9✔
913

914
    def freqresp(self, omega):
9✔
915
        """(deprecated) Evaluate transfer function at complex frequencies.
916

917
        .. deprecated::0.9.0
918
            Method has been given the more pythonic name
919
            :meth:`StateSpace.frequency_response`. Or use
920
            :func:`freqresp` in the MATLAB compatibility module.
921
        """
922
        warn("StateSpace.freqresp(omega) will be removed in a "
5✔
923
             "future release of python-control; use "
924
             "sys.frequency_response(omega), or freqresp(sys, omega) in the "
925
             "MATLAB compatibility module instead", FutureWarning)
926
        return self.frequency_response(omega)
5✔
927

928
    # Compute poles and zeros
929
    def poles(self):
9✔
930
        """Compute the poles of a state space system."""
931

932
        return eigvals(self.A).astype(complex) if self.nstates \
9✔
933
            else np.array([])
934

935
    def zeros(self):
9✔
936
        """Compute the zeros of a state space system."""
937

938
        if not self.nstates:
9✔
939
            return np.array([])
×
940

941
        # Use AB08ND from Slycot if it's available, otherwise use
942
        # scipy.lingalg.eigvals().
943
        try:
9✔
944
            from slycot import ab08nd
9✔
945

946
            out = ab08nd(self.A.shape[0], self.B.shape[1], self.C.shape[0],
5✔
947
                         self.A, self.B, self.C, self.D)
948
            nu = out[0]
5✔
949
            if nu == 0:
5✔
950
                return np.array([])
5✔
951
            else:
952
                # Use SciPy generalized eigenvalue fucntion
953
                return sp.linalg.eigvals(out[8][0:nu, 0:nu],
5✔
954
                                         out[9][0:nu, 0:nu]).astype(complex)
955

956
        except ImportError:  # Slycot unavailable. Fall back to scipy.
4✔
957
            if self.C.shape[0] != self.D.shape[1]:
4✔
958
                raise NotImplementedError(
959
                    "StateSpace.zero only supports systems with the same "
960
                    "number of inputs as outputs.")
961

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

980
    # Feedback around a state space system
981
    def feedback(self, other=1, sign=-1):
9✔
982
        """Feedback interconnection between two LTI systems."""
983
        # Convert the system to state space, if possible
984
        try:
9✔
985
            other = _convert_to_statespace(other)
9✔
986
        except:
9✔
987
            pass
9✔
988

989
        if not isinstance(other, StateSpace):
9✔
990
            return NonlinearIOSystem.feedback(self, other, sign)
9✔
991

992
        # Check to make sure the dimensions are OK
993
        if self.ninputs != other.noutputs or self.noutputs != other.ninputs:
9✔
994
            raise ValueError("State space systems don't have compatible "
×
995
                             "inputs/outputs for feedback.")
996
        dt = common_timebase(self.dt, other.dt)
9✔
997

998
        A1 = self.A
9✔
999
        B1 = self.B
9✔
1000
        C1 = self.C
9✔
1001
        D1 = self.D
9✔
1002
        A2 = other.A
9✔
1003
        B2 = other.B
9✔
1004
        C2 = other.C
9✔
1005
        D2 = other.D
9✔
1006

1007
        F = eye(self.ninputs) - sign * D2 @ D1
9✔
1008
        if matrix_rank(F) != self.ninputs:
9✔
1009
            raise ValueError(
×
1010
                "I - sign * D2 * D1 is singular to working precision.")
1011

1012
        # Precompute F\D2 and F\C2 (E = inv(F))
1013
        # We can solve two linear systems in one pass, since the
1014
        # coefficients matrix F is the same. Thus, we perform the LU
1015
        # decomposition (cubic runtime complexity) of F only once!
1016
        # The remaining back substitutions are only quadratic in runtime.
1017
        E_D2_C2 = solve(F, concatenate((D2, C2), axis=1))
9✔
1018
        E_D2 = E_D2_C2[:, :other.ninputs]
9✔
1019
        E_C2 = E_D2_C2[:, other.ninputs:]
9✔
1020

1021
        T1 = eye(self.noutputs) + sign * D1 @ E_D2
9✔
1022
        T2 = eye(self.ninputs) + sign * E_D2 @ D1
9✔
1023

1024
        A = concatenate(
9✔
1025
            (concatenate(
1026
                (A1 + sign * B1 @ E_D2 @ C1,
1027
                 sign * B1 @ E_C2), axis=1),
1028
             concatenate(
1029
                 (B2 @ T1 @ C1,
1030
                  A2 + sign * B2 @ D1 @ E_C2), axis=1)),
1031
            axis=0)
1032
        B = concatenate((B1 @ T2, B2 @ D1 @ T2), axis=0)
9✔
1033
        C = concatenate((T1 @ C1, sign * D1 @ E_C2), axis=1)
9✔
1034
        D = D1 @ T2
9✔
1035

1036
        return StateSpace(A, B, C, D, dt)
9✔
1037

1038
    def lft(self, other, nu=-1, ny=-1):
9✔
1039
        """Return the Linear Fractional Transformation.
1040

1041
        A definition of the LFT operator can be found in Appendix A.7,
1042
        page 512 in the 2nd Edition, Multivariable Feedback Control by
1043
        Sigurd Skogestad.
1044

1045
        An alternative definition can be found here:
1046
        https://www.mathworks.com/help/control/ref/lft.html
1047

1048
        Parameters
1049
        ----------
1050
        other : LTI
1051
            The lower LTI system
1052
        ny : int, optional
1053
            Dimension of (plant) measurement output.
1054
        nu : int, optional
1055
            Dimension of (plant) control input.
1056

1057
        """
1058
        other = _convert_to_statespace(other)
9✔
1059
        # maximal values for nu, ny
1060
        if ny == -1:
9✔
1061
            ny = min(other.ninputs, self.noutputs)
9✔
1062
        if nu == -1:
9✔
1063
            nu = min(other.noutputs, self.ninputs)
9✔
1064
        # dimension check
1065
        # TODO
1066

1067
        dt = common_timebase(self.dt, other.dt)
9✔
1068

1069
        # submatrices
1070
        A = self.A
9✔
1071
        B1 = self.B[:, :self.ninputs - nu]
9✔
1072
        B2 = self.B[:, self.ninputs - nu:]
9✔
1073
        C1 = self.C[:self.noutputs - ny, :]
9✔
1074
        C2 = self.C[self.noutputs - ny:, :]
9✔
1075
        D11 = self.D[:self.noutputs - ny, :self.ninputs - nu]
9✔
1076
        D12 = self.D[:self.noutputs - ny, self.ninputs - nu:]
9✔
1077
        D21 = self.D[self.noutputs - ny:, :self.ninputs - nu]
9✔
1078
        D22 = self.D[self.noutputs - ny:, self.ninputs - nu:]
9✔
1079

1080
        # submatrices
1081
        Abar = other.A
9✔
1082
        Bbar1 = other.B[:, :ny]
9✔
1083
        Bbar2 = other.B[:, ny:]
9✔
1084
        Cbar1 = other.C[:nu, :]
9✔
1085
        Cbar2 = other.C[nu:, :]
9✔
1086
        Dbar11 = other.D[:nu, :ny]
9✔
1087
        Dbar12 = other.D[:nu, ny:]
9✔
1088
        Dbar21 = other.D[nu:, :ny]
9✔
1089
        Dbar22 = other.D[nu:, ny:]
9✔
1090

1091
        # well-posed check
1092
        F = np.block([[np.eye(ny), -D22], [-Dbar11, np.eye(nu)]])
9✔
1093
        if matrix_rank(F) != ny + nu:
9✔
1094
            raise ValueError("lft not well-posed to working precision.")
×
1095

1096
        # solve for the resulting ss by solving for [y, u] using [x,
1097
        # xbar] and [w1, w2].
1098
        TH = np.linalg.solve(F, np.block(
9✔
1099
            [[C2, np.zeros((ny, other.nstates)),
1100
              D21, np.zeros((ny, other.ninputs - ny))],
1101
             [np.zeros((nu, self.nstates)), Cbar1,
1102
              np.zeros((nu, self.ninputs - nu)), Dbar12]]
1103
        ))
1104
        T11 = TH[:ny, :self.nstates]
9✔
1105
        T12 = TH[:ny, self.nstates: self.nstates + other.nstates]
9✔
1106
        T21 = TH[ny:, :self.nstates]
9✔
1107
        T22 = TH[ny:, self.nstates: self.nstates + other.nstates]
9✔
1108
        H11 = TH[:ny, self.nstates + other.nstates:self.nstates +
9✔
1109
                 other.nstates + self.ninputs - nu]
1110
        H12 = TH[:ny, self.nstates + other.nstates + self.ninputs - nu:]
9✔
1111
        H21 = TH[ny:, self.nstates + other.nstates:self.nstates +
9✔
1112
                 other.nstates + self.ninputs - nu]
1113
        H22 = TH[ny:, self.nstates + other.nstates + self.ninputs - nu:]
9✔
1114

1115
        Ares = np.block([
9✔
1116
            [A + B2 @ T21, B2 @ T22],
1117
            [Bbar1 @ T11, Abar + Bbar1 @ T12]
1118
        ])
1119

1120
        Bres = np.block([
9✔
1121
            [B1 + B2 @ H21, B2 @ H22],
1122
            [Bbar1 @ H11, Bbar2 + Bbar1 @ H12]
1123
        ])
1124

1125
        Cres = np.block([
9✔
1126
            [C1 + D12 @ T21, D12 @ T22],
1127
            [Dbar21 @ T11, Cbar2 + Dbar21 @ T12]
1128
        ])
1129

1130
        Dres = np.block([
9✔
1131
            [D11 + D12 @ H21, D12 @ H22],
1132
            [Dbar21 @ H11, Dbar22 + Dbar21 @ H12]
1133
        ])
1134
        return StateSpace(Ares, Bres, Cres, Dres, dt)
9✔
1135

1136
    def minreal(self, tol=0.0):
9✔
1137
        """Calculate a minimal realization, removes unobservable and
1138
        uncontrollable states"""
1139
        if self.nstates:
9✔
1140
            try:
5✔
1141
                from slycot import tb01pd
5✔
1142
                B = empty((self.nstates, max(self.ninputs, self.noutputs)))
5✔
1143
                B[:, :self.ninputs] = self.B
5✔
1144
                C = empty((max(self.noutputs, self.ninputs), self.nstates))
5✔
1145
                C[:self.noutputs, :] = self.C
5✔
1146
                A, B, C, nr = tb01pd(self.nstates, self.ninputs, self.noutputs,
5✔
1147
                                     self.A, B, C, tol=tol)
1148
                return StateSpace(A[:nr, :nr], B[:nr, :self.ninputs],
5✔
1149
                                  C[:self.noutputs, :nr], self.D)
1150
            except ImportError:
×
1151
                raise TypeError("minreal requires slycot tb01pd")
×
1152
        else:
1153
            return StateSpace(self)
9✔
1154

1155
    def returnScipySignalLTI(self, strict=True):
9✔
1156
        """Return a list of a list of :class:`scipy.signal.lti` objects.
1157

1158
        For instance,
1159

1160
        >>> out = ssobject.returnScipySignalLTI()               # doctest: +SKIP
1161
        >>> out[3][5]                                           # doctest: +SKIP
1162

1163
        is a :class:`scipy.signal.lti` object corresponding to the transfer
1164
        function from the 6th input to the 4th output.
1165

1166
        Parameters
1167
        ----------
1168
        strict : bool, optional
1169
            True (default):
1170
                The timebase `ssobject.dt` cannot be None; it must
1171
                be continuous (0) or discrete (True or > 0).
1172
            False:
1173
              If `ssobject.dt` is None, continuous time
1174
              :class:`scipy.signal.lti` objects are returned.
1175

1176
        Returns
1177
        -------
1178
        out : list of list of :class:`scipy.signal.StateSpace`
1179
            continuous time (inheriting from :class:`scipy.signal.lti`)
1180
            or discrete time (inheriting from :class:`scipy.signal.dlti`)
1181
            SISO objects
1182
        """
1183
        if strict and self.dt is None:
9✔
1184
            raise ValueError("with strict=True, dt cannot be None")
9✔
1185

1186
        if self.dt:
9✔
1187
            kwdt = {'dt': self.dt}
9✔
1188
        else:
1189
            # scipy convention for continuous time lti systems: call without
1190
            # dt keyword argument
1191
            kwdt = {}
9✔
1192

1193
        # Preallocate the output.
1194
        out = [[[] for _ in range(self.ninputs)] for _ in range(self.noutputs)]
9✔
1195

1196
        for i in range(self.noutputs):
9✔
1197
            for j in range(self.ninputs):
9✔
1198
                out[i][j] = signalStateSpace(asarray(self.A),
9✔
1199
                                             asarray(self.B[:, j:j + 1]),
1200
                                             asarray(self.C[i:i + 1, :]),
1201
                                             asarray(self.D[i:i + 1, j:j + 1]),
1202
                                             **kwdt)
1203

1204
        return out
9✔
1205

1206
    def append(self, other):
9✔
1207
        """Append a second model to the present model.
1208

1209
        The second model is converted to state-space if necessary, inputs and
1210
        outputs are appended and their order is preserved"""
1211
        if not isinstance(other, StateSpace):
9✔
1212
            other = _convert_to_statespace(other)
9✔
1213

1214
        self.dt = common_timebase(self.dt, other.dt)
9✔
1215

1216
        n = self.nstates + other.nstates
9✔
1217
        m = self.ninputs + other.ninputs
9✔
1218
        p = self.noutputs + other.noutputs
9✔
1219
        A = zeros((n, n))
9✔
1220
        B = zeros((n, m))
9✔
1221
        C = zeros((p, n))
9✔
1222
        D = zeros((p, m))
9✔
1223
        A[:self.nstates, :self.nstates] = self.A
9✔
1224
        A[self.nstates:, self.nstates:] = other.A
9✔
1225
        B[:self.nstates, :self.ninputs] = self.B
9✔
1226
        B[self.nstates:, self.ninputs:] = other.B
9✔
1227
        C[:self.noutputs, :self.nstates] = self.C
9✔
1228
        C[self.noutputs:, self.nstates:] = other.C
9✔
1229
        D[:self.noutputs, :self.ninputs] = self.D
9✔
1230
        D[self.noutputs:, self.ninputs:] = other.D
9✔
1231
        return StateSpace(A, B, C, D, self.dt)
9✔
1232

1233
    def __getitem__(self, key):
9✔
1234
        """Array style access"""
1235
        if not isinstance(key, Iterable) or len(key) != 2:
9✔
1236
            raise IOError("must provide indices of length 2 for state space")
9✔
1237

1238
        # Convert signal names to integer offsets
1239
        iomap = NamedSignal(self.D, self.output_labels, self.input_labels)
9✔
1240
        indices = iomap._parse_key(key, level=1)  # ignore index checks
9✔
1241
        outdx, output_labels = _process_subsys_index(
9✔
1242
            indices[0], self.output_labels)
1243
        inpdx, input_labels = _process_subsys_index(
9✔
1244
            indices[1], self.input_labels)
1245

1246
        sysname = config.defaults['iosys.indexed_system_name_prefix'] + \
9✔
1247
            self.name + config.defaults['iosys.indexed_system_name_suffix']
1248
        return StateSpace(
9✔
1249
            self.A, self.B[:, inpdx], self.C[outdx, :],
1250
            self.D[outdx, :][:, inpdx], self.dt,
1251
            name=sysname, inputs=input_labels, outputs=output_labels)
1252

1253
    def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
9✔
1254
               name=None, copy_names=True, **kwargs):
1255
        """Convert a continuous time system to discrete time
1256

1257
        Creates a discrete-time system from a continuous-time system by
1258
        sampling.  Multiple methods of conversion are supported.
1259

1260
        Parameters
1261
        ----------
1262
        Ts : float
1263
            Sampling period
1264
        method :  {"gbt", "bilinear", "euler", "backward_diff", "zoh"}
1265
            Which method to use:
1266

1267
            * gbt: generalized bilinear transformation
1268
            * bilinear: Tustin's approximation ("gbt" with alpha=0.5)
1269
            * euler: Euler (or forward differencing) method ("gbt" with
1270
              alpha=0)
1271
            * backward_diff: Backwards differencing ("gbt" with alpha=1.0)
1272
            * zoh: zero-order hold (default)
1273
        alpha : float within [0, 1]
1274
            The generalized bilinear transformation weighting parameter, which
1275
            should only be specified with method="gbt", and is ignored
1276
            otherwise
1277
        prewarp_frequency : float within [0, infinity)
1278
            The frequency [rad/s] at which to match with the input continuous-
1279
            time system's magnitude and phase (the gain=1 crossover frequency,
1280
            for example). Should only be specified with method='bilinear' or
1281
            'gbt' with alpha=0.5 and ignored otherwise.
1282
        name : string, optional
1283
            Set the name of the sampled system.  If not specified and
1284
            if `copy_names` is `False`, a generic name <sys[id]> is generated
1285
            with a unique integer id.  If `copy_names` is `True`, the new system
1286
            name is determined by adding the prefix and suffix strings in
1287
            config.defaults['iosys.sampled_system_name_prefix'] and
1288
            config.defaults['iosys.sampled_system_name_suffix'], with the
1289
            default being to add the suffix '$sampled'.
1290
        copy_names : bool, Optional
1291
            If True, copy the names of the input signals, output
1292
            signals, and states to the sampled system.
1293

1294
        Returns
1295
        -------
1296
        sysd : StateSpace
1297
            Discrete-time system, with sampling rate Ts
1298

1299
        Other Parameters
1300
        ----------------
1301
        inputs : int, list of str or None, optional
1302
            Description of the system inputs.  If not specified, the origional
1303
            system inputs are used.  See :class:`InputOutputSystem` for more
1304
            information.
1305
        outputs : int, list of str or None, optional
1306
            Description of the system outputs.  Same format as `inputs`.
1307
        states : int, list of str, or None, optional
1308
            Description of the system states.  Same format as `inputs`.
1309

1310
        Notes
1311
        -----
1312
        Uses :func:`scipy.signal.cont2discrete`
1313

1314
        Examples
1315
        --------
1316
        >>> G = ct.ss(0, 1, 1, 0)
1317
        >>> sysd = G.sample(0.5, method='bilinear')
1318

1319
        """
1320
        if not self.isctime():
9✔
1321
            raise ValueError("System must be continuous time system")
×
1322
        if prewarp_frequency is not None:
9✔
1323
            if method in ('bilinear', 'tustin') or \
9✔
1324
                    (method == 'gbt' and alpha == 0.5):
1325
                Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency
9✔
1326
            else:
1327
                warn('prewarp_frequency ignored: incompatible conversion')
9✔
1328
                Twarp = Ts
9✔
1329
        else:
1330
            Twarp = Ts
9✔
1331
        sys = (self.A, self.B, self.C, self.D)
9✔
1332
        Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha)
9✔
1333
        sysd = StateSpace(Ad, Bd, C, D, Ts)
9✔
1334
        # copy over the system name, inputs, outputs, and states
1335
        if copy_names:
9✔
1336
            sysd._copy_names(self, prefix_suffix_name='sampled')
9✔
1337
            if name is not None:
9✔
1338
                sysd.name = name
9✔
1339
        # pass desired signal names if names were provided
1340
        return StateSpace(sysd, **kwargs)
9✔
1341

1342
    def dcgain(self, warn_infinite=False):
9✔
1343
        """Return the zero-frequency gain
1344

1345
        The zero-frequency gain of a continuous-time state-space
1346
        system is given by:
1347

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

1350
        and of a discrete-time state-space system by:
1351

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

1354
        Parameters
1355
        ----------
1356
        warn_infinite : bool, optional
1357
            By default, don't issue a warning message if the zero-frequency
1358
            gain is infinite.  Setting `warn_infinite` to generate the warning
1359
            message.
1360

1361
        Returns
1362
        -------
1363
        gain : (noutputs, ninputs) ndarray or scalar
1364
            Array or scalar value for SISO systems, depending on
1365
            config.defaults['control.squeeze_frequency_response'].
1366
            The value of the array elements or the scalar is either the
1367
            zero-frequency (or DC) gain, or `inf`, if the frequency response
1368
            is singular.
1369

1370
            For real valued systems, the empty imaginary part of the
1371
            complex zero-frequency response is discarded and a real array or
1372
            scalar is returned.
1373
        """
1374
        return self._dcgain(warn_infinite)
9✔
1375

1376
    def dynamics(self, t, x, u=None, params=None):
9✔
1377
        """Compute the dynamics of the system
1378

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

1382
            dx/dt = A x + B u
1383

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

1387
            x[t+dt] = A x[t] + B u[t]
1388

1389
        The inputs `x` and `u` must be of the correct length for the system.
1390

1391
        The first argument `t` is ignored because :class:`StateSpace` systems
1392
        are time-invariant. It is included so that the dynamics can be passed
1393
        to numerical integrators, such as :func:`scipy.integrate.solve_ivp`
1394
        and for consistency with :class:`IOSystem` systems.
1395

1396
        Parameters
1397
        ----------
1398
        t : float (ignored)
1399
            time
1400
        x : array_like
1401
            current state
1402
        u : array_like (optional)
1403
            input, zero if omitted
1404

1405
        Returns
1406
        -------
1407
        dx/dt or x[t+dt] : ndarray
1408

1409
        """
1410
        if params is not None:
9✔
1411
            warn("params keyword ignored for StateSpace object")
9✔
1412

1413
        x = np.reshape(x, (-1, 1))  # force to a column in case matrix
9✔
1414
        if np.size(x) != self.nstates:
9✔
1415
            raise ValueError("len(x) must be equal to number of states")
9✔
1416
        if u is None:
9✔
1417
            return (self.A @ x).reshape((-1,))  # return as row vector
9✔
1418
        else:  # received t, x, and u, ignore t
1419
            u = np.reshape(u, (-1, 1))  # force to column in case matrix
9✔
1420
            if np.size(u) != self.ninputs:
9✔
1421
                raise ValueError("len(u) must be equal to number of inputs")
9✔
1422
            return (self.A @ x).reshape((-1,)) \
9✔
1423
                + (self.B @ u).reshape((-1,))  # return as row vector
1424

1425
    def output(self, t, x, u=None, params=None):
9✔
1426
        """Compute the output of the system
1427

1428
        Given input `u` and state `x`, returns the output `y` of the
1429
        state-space system:
1430

1431
            y = C x + D u
1432

1433
        where A and B are the state-space matrices of the system.
1434

1435
        The first argument `t` is ignored because :class:`StateSpace` systems
1436
        are time-invariant. It is included so that the dynamics can be passed
1437
        to most numerical integrators, such as scipy's `integrate.solve_ivp`
1438
        and for consistency with :class:`IOSystem` systems.
1439

1440
        The inputs `x` and `u` must be of the correct length for the system.
1441

1442
        Parameters
1443
        ----------
1444
        t : float (ignored)
1445
            time
1446
        x : array_like
1447
            current state
1448
        u : array_like (optional)
1449
            input (zero if omitted)
1450

1451
        Returns
1452
        -------
1453
        y : ndarray
1454
        """
1455
        if params is not None:
9✔
1456
            warn("params keyword ignored for StateSpace object")
9✔
1457

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

1462
        if u is None:
9✔
1463
            return (self.C @ x).reshape((-1,))  # return as row vector
9✔
1464
        else:  # received t, x, and u, ignore t
1465
            u = np.reshape(u, (-1, 1))  # force to a column in case matrix
9✔
1466
            if np.size(u) != self.ninputs:
9✔
1467
                raise ValueError("len(u) must be equal to number of inputs")
9✔
1468
            return (self.C @ x).reshape((-1,)) \
9✔
1469
                + (self.D @ u).reshape((-1,))  # return as row vector
1470

1471

1472
class LinearICSystem(InterconnectedSystem, StateSpace):
9✔
1473
    """Interconnection of a set of linear input/output systems.
1474

1475
    This class is used to implement a system that is an interconnection of
1476
    linear input/output systems.  It has all of the structure of an
1477
    :class:`~control.InterconnectedSystem`, but also maintains the required
1478
    elements of the :class:`StateSpace` class structure, allowing it to be
1479
    passed to functions that expect a :class:`StateSpace` system.
1480

1481
    This class is generated using :func:`~control.interconnect` and
1482
    not called directly.
1483

1484
    """
1485

1486
    def __init__(self, io_sys, ss_sys=None, connection_type=None):
9✔
1487
        #
1488
        # Because this is a "hybrid" object, the initialization proceeds in
1489
        # stages.  We first create an empty InputOutputSystem of the
1490
        # appropriate size, then copy over the elements of the
1491
        # InterconnectedIOSystem class.  From there we compute the
1492
        # linearization of the system (if needed) and then populate the
1493
        # StateSpace parameters.
1494
        #
1495
        # Create the (essentially empty) I/O system object
1496
        InputOutputSystem.__init__(
9✔
1497
            self, name=io_sys.name, inputs=io_sys.ninputs,
1498
            outputs=io_sys.noutputs, states=io_sys.nstates, dt=io_sys.dt)
1499

1500
        # Copy over the attributes from the interconnected system
1501
        self.syslist = io_sys.syslist
9✔
1502
        self.syslist_index = io_sys.syslist_index
9✔
1503
        self.state_offset = io_sys.state_offset
9✔
1504
        self.input_offset = io_sys.input_offset
9✔
1505
        self.output_offset = io_sys.output_offset
9✔
1506
        self.connect_map = io_sys.connect_map
9✔
1507
        self.input_map = io_sys.input_map
9✔
1508
        self.output_map = io_sys.output_map
9✔
1509
        self.params = io_sys.params
9✔
1510
        self.connection_type = connection_type
9✔
1511

1512
        # If we didnt' get a state space system, linearize the full system
1513
        if ss_sys is None:
9✔
1514
            ss_sys = self.linearize(0, 0)
9✔
1515

1516
        # Initialize the state space object
1517
        StateSpace.__init__(
9✔
1518
            self, ss_sys, name=io_sys.name, inputs=io_sys.input_labels,
1519
            outputs=io_sys.output_labels, states=io_sys.state_labels,
1520
            params=io_sys.params, remove_useless_states=False)
1521

1522
        # Use StateSpace.__call__ to evaluate at a given complex value
1523
        def __call__(self, *args, **kwargs):
9✔
1524
            return StateSpace.__call__(self, *args, **kwargs)
×
1525

1526
    # The following text needs to be replicated from StateSpace in order for
1527
    # this entry to show up properly in sphinx doccumentation (not sure why,
1528
    # but it was the only way to get it to work).
1529
    #
1530
    #: Deprecated attribute; use :attr:`nstates` instead.
1531
    #:
1532
    #: The ``state`` attribute was used to store the number of states for : a
1533
    #: state space system.  It is no longer used.  If you need to access the
1534
    #: number of states, use :attr:`nstates`.
1535
    states = property(StateSpace._get_states, StateSpace._set_states)
9✔
1536

1537

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

1542
    Create a state space system.
1543

1544
    The function accepts either 1, 2, 4 or 5 parameters:
1545

1546
    ``ss(sys)``
1547
        Convert a linear system into space system form. Always creates a
1548
        new system, even if sys is already a state space system.
1549

1550
    ``ss(A, B, C, D)``
1551
        Create a state space system from the matrices of its state and
1552
        output equations:
1553

1554
        .. math::
1555

1556
            dx/dt &= A x + B u \\
1557
                y &= C x + D  u
1558

1559
    ``ss(A, B, C, D, dt)``
1560
        Create a discrete-time state space system from the matrices of
1561
        its state and output equations:
1562

1563
        .. math::
1564

1565
            x[k+1] &= A x[k] + B u[k] \\
1566
              y[k] &= C x[k] + D u[k]
1567

1568
        The matrices can be given as *array like* data types or strings.
1569
        Everything that the constructor of :class:`numpy.matrix` accepts is
1570
        permissible here too.
1571

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

1575
    Parameters
1576
    ----------
1577
    sys : StateSpace or TransferFunction
1578
        A linear system.
1579
    A, B, C, D : array_like or string
1580
        System, control, output, and feed forward matrices.
1581
    dt : None, True or float, optional
1582
        System timebase. 0 (default) indicates continuous
1583
        time, True indicates discrete time with unspecified sampling
1584
        time, positive number is discrete time with specified
1585
        sampling time, None indicates unspecified timebase (either
1586
        continuous or discrete time).
1587
    inputs, outputs, states : str, or list of str, optional
1588
        List of strings that name the individual signals.  If this parameter
1589
        is not given or given as `None`, the signal names will be of the
1590
        form `s[i]` (where `s` is one of `u`, `y`, or `x`). See
1591
        :class:`InputOutputSystem` for more information.
1592
    name : string, optional
1593
        System name (used for specifying signals). If unspecified, a generic
1594
        name <sys[id]> is generated with a unique integer id.
1595
    method : str, optional
1596
        Set the method used for computing the result.  Current methods are
1597
        'slycot' and 'scipy'.  If set to None (default), try 'slycot' first
1598
        and then 'scipy' (SISO only).
1599

1600
    Returns
1601
    -------
1602
    out: :class:`StateSpace`
1603
        Linear input/output system.
1604

1605
    Raises
1606
    ------
1607
    ValueError
1608
        If matrix sizes are not self-consistent.
1609

1610
    See Also
1611
    --------
1612
    tf, ss2tf, tf2ss
1613

1614
    Notes
1615
    -----
1616
    If a transfer function is passed as the sole positional argument, the
1617
    system will be converted to state space form in the same way as calling
1618
    :func:`~control.tf2ss`.  The `method` keyword can be used to select the
1619
    method for conversion.
1620

1621
    Examples
1622
    --------
1623
    Create a Linear I/O system object from matrices.
1624

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

1627
    Convert a TransferFunction to a StateSpace object.
1628

1629
    >>> sys_tf = ct.tf([2.], [1., 3])
1630
    >>> sys2 = ct.ss(sys_tf)
1631

1632
    """
1633
    # See if this is a nonlinear I/O system (legacy usage)
1634
    if len(args) > 0 and (hasattr(args[0], '__call__') or args[0] is None) \
9✔
1635
       and not isinstance(args[0], (InputOutputSystem, LTI)):
1636
        # Function as first (or second) argument => assume nonlinear IO system
1637
        warn("using ss() to create nonlinear I/O systems is deprecated; "
9✔
1638
             "use nlsys()", FutureWarning)
1639
        return NonlinearIOSystem(*args, **kwargs)
9✔
1640

1641
    elif len(args) == 4 or len(args) == 5:
9✔
1642
        # Create a state space function from A, B, C, D[, dt]
1643
        sys = StateSpace(*args, **kwargs)
9✔
1644

1645
    elif len(args) == 1:
9✔
1646
        sys = args[0]
9✔
1647
        if isinstance(sys, LTI):
9✔
1648
            # Check for system with no states and specified state names
1649
            if sys.nstates is None and 'states' in kwargs:
9✔
1650
                warn("state labels specified for "
9✔
1651
                     "non-unique state space realization")
1652

1653
            # Allow method to be specified (eg, tf2ss)
1654
            method = kwargs.pop('method', None)
9✔
1655

1656
            # Create a state space system from an LTI system
1657
            sys = StateSpace(
9✔
1658
                _convert_to_statespace(
1659
                    sys, method=method,
1660
                    use_prefix_suffix=not sys._generic_name_check()),
1661
                **kwargs)
1662

1663
        else:
1664
            raise TypeError("ss(sys): sys must be a StateSpace or "
9✔
1665
                            "TransferFunction object.  It is %s." % type(sys))
1666
    else:
1667
        raise TypeError(
9✔
1668
            "Needs 1, 4, or 5 arguments; received %i." % len(args))
1669

1670
    return sys
9✔
1671

1672

1673
# Convert a state space system into an input/output system (wrapper)
1674
def ss2io(*args, **kwargs):
9✔
1675
    """ss2io(sys[, ...])
1676

1677
    Create an I/O system from a state space linear system.
1678

1679
    .. deprecated:: 0.10.0
1680
        This function will be removed in a future version of python-control.
1681
        The `ss` function can be used directly to produce an I/O system.
1682

1683
    Create an :class:`~control.StateSpace` system with the given signal
1684
    and system names.  See :func:`~control.ss` for more details.
1685
    """
1686
    warn("ss2io() is deprecated; use ss()", FutureWarning)
9✔
1687
    return StateSpace(*args, **kwargs)
9✔
1688

1689

1690
# Convert a transfer function into an input/output system (wrapper)
1691
def tf2io(*args, **kwargs):
9✔
1692
    """tf2io(sys[, ...])
1693

1694
    Convert a transfer function into an I/O system.
1695

1696
    .. deprecated:: 0.10.0
1697
        This function will be removed in a future version of python-control.
1698
        The `tf2ss` function can be used to produce a state space I/O system.
1699

1700
    The function accepts either 1 or 2 parameters:
1701

1702
    ``tf2io(sys)``
1703
        Convert a linear system into space space form. Always creates
1704
        a new system, even if sys is already a StateSpace object.
1705

1706
    ``tf2io(num, den)``
1707
        Create a linear I/O system from its numerator and denominator
1708
        polynomial coefficients.
1709

1710
        For details see: :func:`tf`
1711

1712
    Parameters
1713
    ----------
1714
    sys : LTI (StateSpace or TransferFunction)
1715
        A linear system.
1716
    num : array_like, or list of list of array_like
1717
        Polynomial coefficients of the numerator.
1718
    den : array_like, or list of list of array_like
1719
        Polynomial coefficients of the denominator.
1720

1721
    Returns
1722
    -------
1723
    out : StateSpace
1724
        New I/O system (in state space form).
1725

1726
    Other Parameters
1727
    ----------------
1728
    inputs, outputs : str, or list of str, optional
1729
        List of strings that name the individual signals of the transformed
1730
        system.  If not given, the inputs and outputs are the same as the
1731
        original system.
1732
    name : string, optional
1733
        System name. If unspecified, a generic name <sys[id]> is generated
1734
        with a unique integer id.
1735

1736
    Raises
1737
    ------
1738
    ValueError
1739
        if `num` and `den` have invalid or unequal dimensions, or if an
1740
        invalid number of arguments is passed in.
1741
    TypeError
1742
        if `num` or `den` are of incorrect type, or if sys is not a
1743
        TransferFunction object.
1744

1745
    See Also
1746
    --------
1747
    ss2io
1748
    tf2ss
1749

1750
    Examples
1751
    --------
1752
    >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
1753
    >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
1754
    >>> sys1 = ct.tf2ss(num, den)
1755

1756
    >>> sys_tf = ct.tf(num, den)
1757
    >>> G = ct.tf2ss(sys_tf)
1758
    >>> G.ninputs, G.noutputs, G.nstates
1759
    (2, 2, 8)
1760

1761
    """
1762
    warn("tf2io() is deprecated; use tf2ss() or tf()", FutureWarning)
9✔
1763
    return tf2ss(*args, **kwargs)
9✔
1764

1765

1766
def tf2ss(*args, **kwargs):
9✔
1767
    """tf2ss(sys)
1768

1769
    Transform a transfer function to a state space system.
1770

1771
    The function accepts either 1 or 2 parameters:
1772

1773
    ``tf2ss(sys)``
1774
        Convert a transfer function into space space form.  Equivalent to
1775
        `ss(sys)`.
1776

1777
    ``tf2ss(num, den)``
1778
        Create a state space system from its numerator and denominator
1779
        polynomial coefficients.
1780

1781
        For details see: :func:`tf`
1782

1783
    Parameters
1784
    ----------
1785
    sys : LTI (StateSpace or TransferFunction)
1786
        A linear system
1787
    num : array_like, or list of list of array_like
1788
        Polynomial coefficients of the numerator
1789
    den : array_like, or list of list of array_like
1790
        Polynomial coefficients of the denominator
1791

1792
    Returns
1793
    -------
1794
    out : StateSpace
1795
        New linear system in state space form
1796

1797
    Other Parameters
1798
    ----------------
1799
    inputs, outputs : str, or list of str, optional
1800
        List of strings that name the individual signals of the transformed
1801
        system.  If not given, the inputs and outputs are the same as the
1802
        original system.
1803
    name : string, optional
1804
        System name. If unspecified, a generic name <sys[id]> is generated
1805
        with a unique integer id.
1806
    method : str, optional
1807
        Set the method used for computing the result.  Current methods are
1808
        'slycot' and 'scipy'.  If set to None (default), try 'slycot' first
1809
        and then 'scipy' (SISO only).
1810

1811
    Raises
1812
    ------
1813
    ValueError
1814
        if `num` and `den` have invalid or unequal dimensions, or if an
1815
        invalid number of arguments is passed in
1816
    TypeError
1817
        if `num` or `den` are of incorrect type, or if sys is not a
1818
        TransferFunction object
1819

1820
    See Also
1821
    --------
1822
    ss
1823
    tf
1824
    ss2tf
1825

1826
    Notes
1827
    -----
1828
    The ``slycot`` routine used to convert a transfer function into state
1829
    space form appears to have a bug and in some (rare) instances may not
1830
    return a system with the same poles as the input transfer function.
1831
    For SISO systems, setting ``method=scipy`` can be used as an alternative.
1832

1833
    Examples
1834
    --------
1835
    >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
1836
    >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
1837
    >>> sys1 = ct.tf2ss(num, den)
1838

1839
    >>> sys_tf = ct.tf(num, den)
1840
    >>> sys2 = ct.tf2ss(sys_tf)
1841

1842
    """
1843

1844
    from .xferfcn import TransferFunction
9✔
1845
    if len(args) == 2 or len(args) == 3:
9✔
1846
        # Assume we were given the num, den
1847
        return StateSpace(
5✔
1848
            _convert_to_statespace(TransferFunction(*args)), **kwargs)
1849

1850
    elif len(args) == 1:
9✔
1851
        return ss(*args, **kwargs)
9✔
1852

1853
    else:
1854
        raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))
×
1855

1856

1857
def ssdata(sys):
9✔
1858
    """
1859
    Return state space data objects for a system.
1860

1861
    Parameters
1862
    ----------
1863
    sys : LTI (StateSpace, or TransferFunction)
1864
        LTI system whose data will be returned
1865

1866
    Returns
1867
    -------
1868
    (A, B, C, D): list of matrices
1869
        State space data for the system
1870
    """
1871
    ss = _convert_to_statespace(sys)
9✔
1872
    return ss.A, ss.B, ss.C, ss.D
9✔
1873

1874

1875
def linfnorm(sys, tol=1e-10):
9✔
1876
    """L-infinity norm of a linear system
1877

1878
    Parameters
1879
    ----------
1880
    sys : LTI (StateSpace or TransferFunction)
1881
      system to evalute L-infinity norm of
1882
    tol : real scalar
1883
      tolerance on norm estimate
1884

1885
    Returns
1886
    -------
1887
    gpeak : non-negative scalar
1888
      L-infinity norm
1889
    fpeak : non-negative scalar
1890
      Frequency, in rad/s, at which gpeak occurs
1891

1892
    For stable systems, the L-infinity and H-infinity norms are equal;
1893
    for unstable systems, the H-infinity norm is infinite, while the
1894
    L-infinity norm is finite if the system has no poles on the
1895
    imaginary axis.
1896

1897
    See also
1898
    --------
1899
    slycot.ab13dd : the Slycot routine linfnorm that does the calculation
1900
    """
1901

1902
    if ab13dd is None:
5✔
1903
        raise ControlSlycot("Can't find slycot module 'ab13dd'")
×
1904

1905
    a, b, c, d = ssdata(_convert_to_statespace(sys))
5✔
1906
    e = np.eye(a.shape[0])
5✔
1907

1908
    n = a.shape[0]
5✔
1909
    m = b.shape[1]
5✔
1910
    p = c.shape[0]
5✔
1911

1912
    if n == 0:
5✔
1913
        # ab13dd doesn't accept empty A, B, C, D;
1914
        # static gain case is easy enough to compute
1915
        gpeak = scipy.linalg.svdvals(d)[0]
5✔
1916
        # max svd is constant with freq; arbitrarily choose 0 as peak
1917
        fpeak = 0
5✔
1918
        return gpeak, fpeak
5✔
1919

1920
    dico = 'C' if sys.isctime() else 'D'
5✔
1921
    jobe = 'I'
5✔
1922
    equil = 'S'
5✔
1923
    jobd = 'Z' if all(0 == d.flat) else 'D'
5✔
1924

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

1927
    if dico=='D':
5✔
1928
        fpeak /= sys.dt
5✔
1929

1930
    return gpeak, fpeak
5✔
1931

1932

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

1936
    Parameters
1937
    ----------
1938
    states, outputs, inputs : int, list of str, or None
1939
        Description of the system states, outputs, and inputs. This can be
1940
        given as an integer count or as a list of strings that name the
1941
        individual signals.  If an integer count is specified, the names of
1942
        the signal will be of the form 's[i]' (where 's' is one of 'x',
1943
        'y', or 'u').
1944
    strictly_proper : bool, optional
1945
        If set to 'True', returns a proper system (no direct term).
1946
    dt : None, True or float, optional
1947
        System timebase. 0 (default) indicates continuous
1948
        time, True indicates discrete time with unspecified sampling
1949
        time, positive number is discrete time with specified
1950
        sampling time, None indicates unspecified timebase (either
1951
        continuous or discrete time).
1952
    name : string, optional
1953
        System name (used for specifying signals). If unspecified, a generic
1954
        name <sys[id]> is generated with a unique integer id.
1955

1956
    Returns
1957
    -------
1958
    sys : StateSpace
1959
        The randomly created linear system.
1960

1961
    Raises
1962
    ------
1963
    ValueError
1964
        if any input is not a positive integer.
1965

1966
    Notes
1967
    -----
1968
    If the number of states, inputs, or outputs is not specified, then the
1969
    missing numbers are assumed to be 1.  If dt is not specified or is given
1970
    as 0 or None, the poles of the returned system will always have a
1971
    negative real part.  If dt is True or a postive float, the poles of the
1972
    returned system will have magnitude less than 1.
1973

1974
    """
1975
    # Process keyword arguments
1976
    kwargs.update({'states': states, 'outputs': outputs, 'inputs': inputs})
9✔
1977
    name, inputs, outputs, states, dt = _process_iosys_keywords(kwargs)
9✔
1978

1979
    # Figure out the size of the sytem
1980
    nstates, _ = _process_signal_list(states)
9✔
1981
    ninputs, _ = _process_signal_list(inputs)
9✔
1982
    noutputs, _ = _process_signal_list(outputs)
9✔
1983

1984
    sys = _rss_generate(
9✔
1985
        nstates, ninputs, noutputs, 'c' if not dt else 'd', name=name,
1986
        strictly_proper=strictly_proper)
1987

1988
    return StateSpace(
9✔
1989
        sys, name=name, states=states, inputs=inputs, outputs=outputs, dt=dt,
1990
        **kwargs)
1991

1992

1993
def drss(*args, **kwargs):
9✔
1994
    """
1995
    drss([states, outputs, inputs, strictly_proper])
1996

1997
    Create a stable, discrete-time, random state space system.
1998

1999
    Create a stable *discrete time* random state space object.  This
2000
    function calls :func:`rss` using either the `dt` keyword provided by
2001
    the user or `dt=True` if not specified.
2002

2003
    Examples
2004
    --------
2005
    >>> G = ct.drss(states=4, outputs=2, inputs=1)
2006
    >>> G.ninputs, G.noutputs, G.nstates
2007
    (1, 2, 4)
2008
    >>> G.isdtime()
2009
    True
2010

2011

2012
    """
2013
    # Make sure the timebase makes sense
2014
    if 'dt' in kwargs:
9✔
2015
        dt = kwargs['dt']
9✔
2016

2017
        if dt == 0:
9✔
2018
            raise ValueError("drss called with continuous timebase")
9✔
2019
        elif dt is None:
9✔
2020
            warn("drss called with unspecified timebase; "
9✔
2021
                 "system may be interpreted as continuous time")
2022
            kwargs['dt'] = True     # force rss to generate discrete time sys
9✔
2023
    else:
2024
        dt = True
9✔
2025
        kwargs['dt'] = True
9✔
2026

2027
    # Create the system
2028
    sys = rss(*args, **kwargs)
9✔
2029

2030
    # Reset the timebase (in case it was specified as None)
2031
    sys.dt = dt
9✔
2032

2033
    return sys
9✔
2034

2035

2036
# Summing junction
2037
def summing_junction(
9✔
2038
        inputs=None, output=None, dimension=None, prefix='u', **kwargs):
2039
    """Create a summing junction as an input/output system.
2040

2041
    This function creates a static input/output system that outputs the sum of
2042
    the inputs, potentially with a change in sign for each individual input.
2043
    The input/output system that is created by this function can be used as a
2044
    component in the :func:`~control.interconnect` function.
2045

2046
    Parameters
2047
    ----------
2048
    inputs : int, string or list of strings
2049
        Description of the inputs to the summing junction.  This can be given
2050
        as an integer count, a string, or a list of strings. If an integer
2051
        count is specified, the names of the input signals will be of the form
2052
        `u[i]`.
2053
    output : string, optional
2054
        Name of the system output.  If not specified, the output will be 'y'.
2055
    dimension : int, optional
2056
        The dimension of the summing junction.  If the dimension is set to a
2057
        positive integer, a multi-input, multi-output summing junction will be
2058
        created.  The input and output signal names will be of the form
2059
        `<signal>[i]` where `signal` is the input/output signal name specified
2060
        by the `inputs` and `output` keywords.  Default value is `None`.
2061
    name : string, optional
2062
        System name (used for specifying signals). If unspecified, a generic
2063
        name <sys[id]> is generated with a unique integer id.
2064
    prefix : string, optional
2065
        If `inputs` is an integer, create the names of the states using the
2066
        given prefix (default = 'u').  The names of the input will be of the
2067
        form `prefix[i]`.
2068

2069
    Returns
2070
    -------
2071
    sys : static StateSpace
2072
        Linear input/output system object with no states and only a direct
2073
        term that implements the summing junction.
2074

2075
    Examples
2076
    --------
2077
    >>> P = ct.tf(1, [1, 0], inputs='u', outputs='y')
2078
    >>> C = ct.tf(10, [1, 1], inputs='e', outputs='u')
2079
    >>> sumblk = ct.summing_junction(inputs=['r', '-y'], output='e')
2080
    >>> T = ct.interconnect([P, C, sumblk], inputs='r', outputs='y')
2081
    >>> T.ninputs, T.noutputs, T.nstates
2082
    (1, 1, 2)
2083

2084
    """
2085
    # Utility function to parse input and output signal lists
2086
    def _parse_list(signals, signame='input', prefix='u'):
9✔
2087
        # Parse signals, including gains
2088
        if isinstance(signals, int):
9✔
2089
            nsignals = signals
9✔
2090
            names = ["%s[%d]" % (prefix, i) for i in range(nsignals)]
9✔
2091
            gains = np.ones((nsignals,))
9✔
2092
        elif isinstance(signals, str):
9✔
2093
            nsignals = 1
×
2094
            gains = [-1 if signals[0] == '-' else 1]
×
2095
            names = [signals[1:] if signals[0] == '-' else signals]
×
2096
        elif isinstance(signals, list) and \
9✔
2097
             all([isinstance(x, str) for x in signals]):
2098
            nsignals = len(signals)
9✔
2099
            gains = np.ones((nsignals,))
9✔
2100
            names = []
9✔
2101
            for i in range(nsignals):
9✔
2102
                if signals[i][0] == '-':
9✔
2103
                    gains[i] = -1
9✔
2104
                    names.append(signals[i][1:])
9✔
2105
                else:
2106
                    names.append(signals[i])
9✔
2107
        else:
2108
            raise ValueError(
9✔
2109
                "could not parse %s description '%s'"
2110
                % (signame, str(signals)))
2111

2112
        # Return the parsed list
2113
        return nsignals, names, gains
9✔
2114

2115
    # Parse system and signal names (with some minor pre-processing)
2116
    if input is not None:
9✔
2117
        kwargs['inputs'] = inputs       # positional/keyword -> keyword
9✔
2118
    if output is not None:
9✔
2119
        kwargs['output'] = output       # positional/keyword -> keyword
9✔
2120
    name, inputs, output, states, dt = _process_iosys_keywords(
9✔
2121
        kwargs, {'inputs': None, 'outputs': 'y'}, end=True)
2122
    if inputs is None:
9✔
2123
        raise TypeError("input specification is required")
9✔
2124

2125
    # Read the input list
2126
    ninputs, input_names, input_gains = _parse_list(
9✔
2127
        inputs, signame="input", prefix=prefix)
2128
    noutputs, output_names, output_gains = _parse_list(
9✔
2129
        output, signame="output", prefix='y')
2130
    if noutputs > 1:
9✔
2131
        raise NotImplementedError("vector outputs not yet supported")
2132

2133
    # If the dimension keyword is present, vectorize inputs and outputs
2134
    if isinstance(dimension, int) and dimension >= 1:
9✔
2135
        # Create a new list of input/output names and update parameters
2136
        input_names = ["%s[%d]" % (name, dim)
9✔
2137
                       for name in input_names
2138
                       for dim in range(dimension)]
2139
        ninputs = ninputs * dimension
9✔
2140

2141
        output_names = ["%s[%d]" % (name, dim)
9✔
2142
                        for name in output_names
2143
                        for dim in range(dimension)]
2144
        noutputs = noutputs * dimension
9✔
2145
    elif dimension is not None:
9✔
2146
        raise ValueError(
9✔
2147
            "unrecognized dimension value '%s'" % str(dimension))
2148
    else:
2149
        dimension = 1
9✔
2150

2151
    # Create the direct term
2152
    D = np.kron(input_gains * output_gains[0], np.eye(dimension))
9✔
2153

2154
    # Create a linear system of the appropriate size
2155
    ss_sys = StateSpace(
9✔
2156
        np.zeros((0, 0)), np.ones((0, ninputs)), np.ones((noutputs, 0)), D)
2157

2158
    # Create a StateSpace
2159
    return StateSpace(
9✔
2160
        ss_sys, inputs=input_names, outputs=output_names, name=name)
2161

2162
#
2163
# Utility functions
2164
#
2165

2166
def _ssmatrix(data, axis=1):
9✔
2167
    """Convert argument to a (possibly empty) 2D state space matrix.
2168

2169
    The axis keyword argument makes it convenient to specify that if the input
2170
    is a vector, it is a row (axis=1) or column (axis=0) vector.
2171

2172
    Parameters
2173
    ----------
2174
    data : array, list, or string
2175
        Input data defining the contents of the 2D array
2176
    axis : 0 or 1
2177
        If input data is 1D, which axis to use for return object.  The default
2178
        is 1, corresponding to a row matrix.
2179

2180
    Returns
2181
    -------
2182
    arr : 2D array, with shape (0, 0) if a is empty
2183

2184
    """
2185
    # Convert the data into an array
2186
    arr = np.array(data, dtype=float)
9✔
2187
    ndim = arr.ndim
9✔
2188
    shape = arr.shape
9✔
2189

2190
    # Change the shape of the array into a 2D array
2191
    if (ndim > 2):
9✔
2192
        raise ValueError("state-space matrix must be 2-dimensional")
×
2193

2194
    elif (ndim == 2 and shape == (1, 0)) or \
9✔
2195
         (ndim == 1 and shape == (0, )):
2196
        # Passed an empty matrix or empty vector; change shape to (0, 0)
2197
        shape = (0, 0)
9✔
2198

2199
    elif ndim == 1:
9✔
2200
        # Passed a row or column vector
2201
        shape = (1, shape[0]) if axis == 1 else (shape[0], 1)
9✔
2202

2203
    elif ndim == 0:
9✔
2204
        # Passed a constant; turn into a matrix
2205
        shape = (1, 1)
9✔
2206

2207
    #  Create the actual object used to store the result
2208
    return arr.reshape(shape)
9✔
2209

2210

2211
def _f2s(f):
9✔
2212
    """Format floating point number f for StateSpace._repr_latex_.
2213

2214
    Numbers are converted to strings with statesp.latex_num_format.
2215

2216
    Inserts column separators, etc., as needed.
2217
    """
2218
    fmt = "{:" + config.defaults['statesp.latex_num_format'] + "}"
9✔
2219
    sraw = fmt.format(f)
9✔
2220
    # significand-exponent
2221
    se = sraw.lower().split('e')
9✔
2222
    # whole-fraction
2223
    wf = se[0].split('.')
9✔
2224
    s = wf[0]
9✔
2225
    if wf[1:]:
9✔
2226
        s += r'.&\hspace{{-1em}}{frac}'.format(frac=wf[1])
9✔
2227
    else:
2228
        s += r'\phantom{.}&\hspace{-1em}'
9✔
2229

2230
    if se[1:]:
9✔
2231
        s += r'&\hspace{{-1em}}\cdot10^{{{:d}}}'.format(int(se[1]))
9✔
2232
    else:
2233
        s += r'&\hspace{-1em}\phantom{\cdot}'
9✔
2234

2235
    return s
9✔
2236

2237

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

2241
    If sys is already a state space, then it is returned.  If sys is a
2242
    transfer function object, then it is converted to a state space and
2243
    returned.
2244

2245
    Note: no renaming of inputs and outputs is performed; this should be done
2246
    by the calling function.
2247

2248
    """
2249
    import itertools
9✔
2250

2251
    from .xferfcn import TransferFunction
9✔
2252

2253
    if isinstance(sys, StateSpace):
9✔
2254
        return sys
9✔
2255

2256
    elif isinstance(sys, TransferFunction):
9✔
2257
        # Make sure the transfer function is proper
2258
        if any([[len(num) for num in col] for col in sys.num] >
9✔
2259
               [[len(num) for num in col] for col in sys.den]):
2260
            raise ValueError("transfer function is non-proper; can't "
9✔
2261
                             "convert to StateSpace system")
2262

2263
        if method is None and slycot_check() or method == 'slycot':
9✔
2264
            if not slycot_check():
9✔
2265
                raise ValueError("method='slycot' requires slycot")
4✔
2266

2267
            from slycot import td04ad
5✔
2268

2269
            # Change the numerator and denominator arrays so that the transfer
2270
            # function matrix has a common denominator.
2271
            # matrices are also sized/padded to fit td04ad
2272
            num, den, denorder = sys.minreal()._common_den()
5✔
2273
            num, den, denorder = sys._common_den()
5✔
2274

2275
            # transfer function to state space conversion now should work!
2276
            ssout = td04ad('C', sys.ninputs, sys.noutputs,
5✔
2277
                           denorder, den, num, tol=0)
2278

2279
            states = ssout[0]
5✔
2280
            newsys = StateSpace(
5✔
2281
                ssout[1][:states, :states], ssout[2][:states, :sys.ninputs],
2282
                ssout[3][:sys.noutputs, :states], ssout[4], sys.dt)
2283

2284
        elif method in [None, 'scipy']:
9✔
2285
            # Scipy tf->ss can't handle MIMO, but SISO is OK
2286
            maxn = max(max(len(n) for n in nrow)
9✔
2287
                       for nrow in sys.num)
2288
            maxd = max(max(len(d) for d in drow)
9✔
2289
                       for drow in sys.den)
2290
            if 1 == maxn and 1 == maxd:
9✔
2291
                D = empty((sys.noutputs, sys.ninputs), dtype=float)
4✔
2292
                for i, j in itertools.product(range(sys.noutputs),
4✔
2293
                                              range(sys.ninputs)):
2294
                    D[i, j] = sys.num[i][j][0] / sys.den[i][j][0]
4✔
2295
                newsys = StateSpace([], [], [], D, sys.dt)
4✔
2296
            else:
2297
                if not issiso(sys):
9✔
2298
                    raise ControlMIMONotImplemented(
4✔
2299
                        "MIMO system conversion not supported without Slycot")
2300

2301
                A, B, C, D = \
9✔
2302
                    sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den))
2303
                newsys = StateSpace(A, B, C, D, sys.dt)
9✔
2304
        else:
2305
            raise ValueError(f"unknown {method=}")
×
2306

2307
        # Copy over the signal (and system) names
2308
        newsys._copy_names(
9✔
2309
            sys,
2310
            prefix_suffix_name='converted' if use_prefix_suffix else None)
2311
        return newsys
9✔
2312

2313
    elif isinstance(sys, FrequencyResponseData):
9✔
2314
        raise TypeError("Can't convert FRD to StateSpace system.")
×
2315

2316
    # If this is a matrix, try to create a constant feedthrough
2317
    try:
9✔
2318
        D = _ssmatrix(np.atleast_2d(sys))
9✔
2319
        return StateSpace([], [], [], D, dt=None)
9✔
2320

2321
    except Exception:
9✔
2322
        raise TypeError("Can't convert given type to StateSpace system.")
9✔
2323

2324

2325
def _rss_generate(
9✔
2326
        states, inputs, outputs, cdtype, strictly_proper=False, name=None):
2327
    """Generate a random state space.
2328

2329
    This does the actual random state space generation expected from rss and
2330
    drss.  cdtype is 'c' for continuous systems and 'd' for discrete systems.
2331

2332
    """
2333

2334
    # Probability of repeating a previous root.
2335
    pRepeat = 0.05
9✔
2336
    # Probability of choosing a real root.  Note that when choosing a complex
2337
    # root, the conjugate gets chosen as well.  So the expected proportion of
2338
    # real roots is pReal / (pReal + 2 * (1 - pReal)).
2339
    pReal = 0.6
9✔
2340
    # Probability that an element in B or C will not be masked out.
2341
    pBCmask = 0.8
9✔
2342
    # Probability that an element in D will not be masked out.
2343
    pDmask = 0.3
9✔
2344
    # Probability that D = 0.
2345
    pDzero = 0.5
9✔
2346

2347
    # Check for valid input arguments.
2348
    if states < 1 or states % 1:
9✔
2349
        raise ValueError("states must be a positive integer.  states = %g." %
9✔
2350
                         states)
2351
    if inputs < 1 or inputs % 1:
9✔
2352
        raise ValueError("inputs must be a positive integer.  inputs = %g." %
9✔
2353
                         inputs)
2354
    if outputs < 1 or outputs % 1:
9✔
2355
        raise ValueError("outputs must be a positive integer.  outputs = %g." %
9✔
2356
                         outputs)
2357
    if cdtype not in ['c', 'd']:
9✔
2358
        raise ValueError("cdtype must be `c` or `d`")
9✔
2359

2360
    # Make some poles for A.  Preallocate a complex array.
2361
    poles = zeros(states) + zeros(states) * 0.j
9✔
2362
    i = 0
9✔
2363

2364
    while i < states:
9✔
2365
        if rand() < pRepeat and i != 0 and i != states - 1:
9✔
2366
            # Small chance of copying poles, if we're not at the first or last
2367
            # element.
2368
            if poles[i-1].imag == 0:
9✔
2369
                # Copy previous real pole.
2370
                poles[i] = poles[i-1]
9✔
2371
                i += 1
9✔
2372
            else:
2373
                # Copy previous complex conjugate pair of poles.
2374
                poles[i:i+2] = poles[i-2:i]
9✔
2375
                i += 2
9✔
2376
        elif rand() < pReal or i == states - 1:
9✔
2377
            # No-oscillation pole.
2378
            if cdtype == 'c':
9✔
2379
                poles[i] = -exp(randn()) + 0.j
9✔
2380
            else:
2381
                poles[i] = 2. * rand() - 1.
9✔
2382
            i += 1
9✔
2383
        else:
2384
            # Complex conjugate pair of oscillating poles.
2385
            if cdtype == 'c':
9✔
2386
                poles[i] = complex(-exp(randn()), 3. * exp(randn()))
9✔
2387
            else:
2388
                mag = rand()
9✔
2389
                phase = 2. * math.pi * rand()
9✔
2390
                poles[i] = complex(mag * cos(phase), mag * sin(phase))
9✔
2391
            poles[i+1] = complex(poles[i].real, -poles[i].imag)
9✔
2392
            i += 2
9✔
2393

2394
    # Now put the poles in A as real blocks on the diagonal.
2395
    A = zeros((states, states))
9✔
2396
    i = 0
9✔
2397
    while i < states:
9✔
2398
        if poles[i].imag == 0:
9✔
2399
            A[i, i] = poles[i].real
9✔
2400
            i += 1
9✔
2401
        else:
2402
            A[i, i] = A[i+1, i+1] = poles[i].real
9✔
2403
            A[i, i+1] = poles[i].imag
9✔
2404
            A[i+1, i] = -poles[i].imag
9✔
2405
            i += 2
9✔
2406
    # Finally, apply a transformation so that A is not block-diagonal.
2407
    while True:
9✔
2408
        T = randn(states, states)
9✔
2409
        try:
9✔
2410
            A = solve(T, A) @ T  # A = T \ A @ T
9✔
2411
            break
9✔
2412
        except LinAlgError:
×
2413
            # In the unlikely event that T is rank-deficient, iterate again.
2414
            pass
×
2415

2416
    # Make the remaining matrices.
2417
    B = randn(states, inputs)
9✔
2418
    C = randn(outputs, states)
9✔
2419
    D = randn(outputs, inputs)
9✔
2420

2421
    # Make masks to zero out some of the elements.
2422
    while True:
9✔
2423
        Bmask = rand(states, inputs) < pBCmask
9✔
2424
        if any(Bmask):  # Retry if we get all zeros.
9✔
2425
            break
9✔
2426
    while True:
9✔
2427
        Cmask = rand(outputs, states) < pBCmask
9✔
2428
        if any(Cmask):  # Retry if we get all zeros.
9✔
2429
            break
9✔
2430
    if rand() < pDzero:
9✔
2431
        Dmask = zeros((outputs, inputs))
9✔
2432
    else:
2433
        Dmask = rand(outputs, inputs) < pDmask
9✔
2434

2435
    # Apply masks.
2436
    B = B * Bmask
9✔
2437
    C = C * Cmask
9✔
2438
    D = D * Dmask if not strictly_proper else zeros(D.shape)
9✔
2439

2440
    if cdtype == 'c':
9✔
2441
        ss_args = (A, B, C, D)
9✔
2442
    else:
2443
        ss_args = (A, B, C, D, True)
9✔
2444
    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