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

python-control / python-control / 12208837637

07 Dec 2024 01:45AM UTC coverage: 94.713% (-0.008%) from 94.721%
12208837637

Pull #1078

github

web-flow
Merge 9acbfbbbf into 12dda4e3e
Pull Request #1078: fix issue with multiplying MIMO LTI system by scalar

9226 of 9741 relevant lines covered (94.71%)

8.27 hits per line

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

97.33
control/xferfcn.py
1
"""xferfcn.py
2

3
Transfer function representation and functions.
4

5
This file contains the TransferFunction class and also functions
6
that operate on transfer functions.  This is the primary representation
7
for the python-control library.
8
"""
9

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

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

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

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

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

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

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

46
$Id$
47

48
"""
49

50
from collections.abc import Iterable
9✔
51
from copy import deepcopy
9✔
52
from itertools import chain
9✔
53
from re import sub
9✔
54
from warnings import warn
9✔
55

56
# External function declarations
57
import numpy as np
9✔
58
import scipy as sp
9✔
59
from numpy import angle, array, delete, empty, exp, finfo, ndarray, nonzero, \
9✔
60
    ones, pi, poly, polyadd, polymul, polyval, real, roots, sqrt, squeeze, \
61
    where, zeros
62
from scipy.signal import TransferFunction as signalTransferFunction
9✔
63
from scipy.signal import cont2discrete, tf2zpk, zpk2tf
9✔
64

65
from . import config
9✔
66
from .exception import ControlMIMONotImplemented
9✔
67
from .frdata import FrequencyResponseData
9✔
68
from .iosys import InputOutputSystem, NamedSignal, _process_iosys_keywords, \
9✔
69
    _process_subsys_index, common_timebase, isdtime
70
from .lti import LTI, _process_frequency_response
9✔
71

72
__all__ = ['TransferFunction', 'tf', 'zpk', 'ss2tf', 'tfdata']
9✔
73

74

75
# Define module default parameter values
76
_xferfcn_defaults = {
9✔
77
    'xferfcn.display_format': 'poly',
78
    'xferfcn.floating_point_format': '.4g'
79
}
80

81

82
class TransferFunction(LTI):
9✔
83
    """TransferFunction(num, den[, dt])
84

85
    A class for representing transfer functions.
86

87
    The TransferFunction class is used to represent systems in transfer
88
    function form.
89

90
    Parameters
91
    ----------
92
    num : array_like, or list of list of array_like
93
        Polynomial coefficients of the numerator
94
    den : array_like, or list of list of array_like
95
        Polynomial coefficients of the denominator
96
    dt : None, True or float, optional
97
        System timebase. 0 (default) indicates continuous
98
        time, True indicates discrete time with unspecified sampling
99
        time, positive number is discrete time with specified
100
        sampling time, None indicates unspecified timebase (either
101
        continuous or discrete time).
102
    display_format : None, 'poly' or 'zpk', optional
103
        Set the display format used in printing the TransferFunction object.
104
        Default behavior is polynomial display and can be changed by
105
        changing config.defaults['xferfcn.display_format'].
106

107
    Attributes
108
    ----------
109
    ninputs, noutputs, nstates : int
110
        Number of input, output and state variables.
111
    num, den : 2D list of array
112
        Polynomial coefficients of the numerator and denominator.
113
    dt : None, True or float
114
        System timebase. 0 (default) indicates continuous time, True indicates
115
        discrete time with unspecified sampling time, positive number is
116
        discrete time with specified sampling time, None indicates unspecified
117
        timebase (either continuous or discrete time).
118

119
    Notes
120
    -----
121
    The attribues 'num' and 'den' are 2-D lists of arrays containing MIMO
122
    numerator and denominator coefficients.  For example,
123

124
    >>> num[2][5] = numpy.array([1., 4., 8.])                   # doctest: +SKIP
125

126
    means that the numerator of the transfer function from the 6th input to
127
    the 3rd output is set to s^2 + 4s + 8.
128

129
    A discrete time transfer function is created by specifying a nonzero
130
    'timebase' dt when the system is constructed:
131

132
    * dt = 0: continuous time system (default)
133
    * dt > 0: discrete time system with sampling period 'dt'
134
    * dt = True: discrete time with unspecified sampling period
135
    * dt = None: no timebase specified
136

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

146
    A transfer function is callable and returns the value of the transfer
147
    function evaluated at a point in the complex plane.  See
148
    :meth:`~control.TransferFunction.__call__` for a more detailed description.
149

150
    Subsystems corresponding to selected input/output pairs can be
151
    created by indexing the transfer function::
152

153
        subsys = sys[output_spec, input_spec]
154

155
    The input and output specifications can be single integers, lists of
156
    integers, or slices.  In addition, the strings representing the names
157
    of the signals can be used and will be replaced with the equivalent
158
    signal offsets.
159

160
    The TransferFunction class defines two constants ``s`` and ``z`` that
161
    represent the differentiation and delay operators in continuous and
162
    discrete time.  These can be used to create variables that allow algebraic
163
    creation of transfer functions.  For example,
164

165
    >>> s = ct.TransferFunction.s
166
    >>> G = (s + 1)/(s**2 + 2*s + 1)
167

168
    """
169
    def __init__(self, *args, **kwargs):
9✔
170
        """TransferFunction(num, den[, dt])
171

172
        Construct a transfer function.
173

174
        The default constructor is TransferFunction(num, den), where num and
175
        den are lists of lists of arrays containing polynomial coefficients.
176
        To create a discrete time transfer funtion, use TransferFunction(num,
177
        den, dt) where 'dt' is the sampling time (or True for unspecified
178
        sampling time).  To call the copy constructor, call
179
        TransferFunction(sys), where sys is a TransferFunction object
180
        (continuous or discrete).
181

182
        """
183
        #
184
        # Process positional arguments
185
        #
186

187
        if len(args) == 2:
9✔
188
            # The user provided a numerator and a denominator.
189
            num, den = args
9✔
190

191
        elif len(args) == 3:
9✔
192
            # Discrete time transfer function
193
            num, den, dt = args
9✔
194
            if 'dt' in kwargs:
9✔
195
                warn("received multiple dt arguments, "
9✔
196
                     "using positional arg dt = %s" % dt)
197
            kwargs['dt'] = dt
9✔
198
            args = args[:-1]
9✔
199

200
        elif len(args) == 1:
9✔
201
            # Use the copy constructor.
202
            if not isinstance(args[0], TransferFunction):
9✔
203
                raise TypeError("The one-argument constructor can only take \
9✔
204
                        in a TransferFunction object.  Received %s."
205
                                % type(args[0]))
206
            num = args[0].num
9✔
207
            den = args[0].den
9✔
208

209
        else:
210
            raise TypeError("Needs 1, 2 or 3 arguments; received %i."
9✔
211
                             % len(args))
212

213
        num = _clean_part(num)
9✔
214
        den = _clean_part(den)
9✔
215

216
        #
217
        # Process keyword arguments
218
        #
219
        # During module init, TransferFunction.s and TransferFunction.z
220
        # get initialized when defaults are not fully initialized yet.
221
        # Use 'poly' in these cases.
222

223
        self.display_format = kwargs.pop(
9✔
224
            'display_format',
225
            config.defaults.get('xferfcn.display_format', 'poly'))
226

227
        if self.display_format not in ('poly', 'zpk'):
9✔
228
            raise ValueError("display_format must be 'poly' or 'zpk',"
×
229
                             " got '%s'" % self.display_format)
230

231
        # Determine if the transfer function is static (needed for dt)
232
        static = True
9✔
233
        for col in num + den:
9✔
234
            for poly in col:
9✔
235
                if len(poly) > 1:
9✔
236
                    static = False
9✔
237

238
        defaults = args[0] if len(args) == 1 else \
9✔
239
            {'inputs': len(num[0]), 'outputs': len(num)}
240

241
        name, inputs, outputs, states, dt = _process_iosys_keywords(
9✔
242
                kwargs, defaults, static=static)
243
        if states:
9✔
244
            raise TypeError(
×
245
                "states keyword not allowed for transfer functions")
246

247
        # Initialize LTI (InputOutputSystem) object
248
        super().__init__(
9✔
249
            name=name, inputs=inputs, outputs=outputs, dt=dt, **kwargs)
250

251
        #
252
        # Check to make sure everything is consistent
253
        #
254
        # Make sure numerator and denominator matrices have consistent sizes
255
        if self.ninputs != len(den[0]):
9✔
256
            raise ValueError(
9✔
257
                "The numerator has %i input(s), but the denominator has "
258
                "%i input(s)." % (self.ninputs, len(den[0])))
259
        if self.noutputs != len(den):
9✔
260
            raise ValueError(
9✔
261
                "The numerator has %i output(s), but the denominator has "
262
                "%i output(s)." % (self.noutputs, len(den)))
263

264
        # Additional checks/updates on structure of the transfer function
265
        for i in range(self.noutputs):
9✔
266
            # Make sure that each row has the same number of columns
267
            if len(num[i]) != self.ninputs:
9✔
268
                raise ValueError(
9✔
269
                    "Row 0 of the numerator matrix has %i elements, but row "
270
                    "%i has %i." % (self.ninputs, i, len(num[i])))
271
            if len(den[i]) != self.ninputs:
9✔
272
                raise ValueError(
9✔
273
                    "Row 0 of the denominator matrix has %i elements, but row "
274
                    "%i has %i." % (self.ninputs, i, len(den[i])))
275

276
            # Check for zeros in numerator or denominator
277
            # TODO: Right now these checks are only done during construction.
278
            # It might be worthwhile to think of a way to perform checks if the
279
            # user modifies the transfer function after construction.
280
            for j in range(self.ninputs):
9✔
281
                # Check that we don't have any zero denominators.
282
                zeroden = True
9✔
283
                for k in den[i][j]:
9✔
284
                    if k:
9✔
285
                        zeroden = False
9✔
286
                        break
9✔
287
                if zeroden:
9✔
288
                    raise ValueError(
9✔
289
                        "Input %i, output %i has a zero denominator."
290
                        % (j + 1, i + 1))
291

292
                # If we have zero numerators, set the denominator to 1.
293
                zeronum = True
9✔
294
                for k in num[i][j]:
9✔
295
                    if k:
9✔
296
                        zeronum = False
9✔
297
                        break
9✔
298
                if zeronum:
9✔
299
                    den[i][j] = ones(1)
9✔
300

301
        # Store the numerator and denominator
302
        self.num = num
9✔
303
        self.den = den
9✔
304

305
        #
306
        # Final processing
307
        #
308
        # Truncate leading zeros
309
        self._truncatecoeff()
9✔
310

311
    #
312
    # Class attributes
313
    #
314
    # These attributes are defined as class attributes so that they are
315
    # documented properly.  They are "overwritten" in __init__.
316
    #
317

318
    #: Number of system inputs.
319
    #:
320
    #: :meta hide-value:
321
    ninputs = 1
9✔
322

323
    #: Number of system outputs.
324
    #:
325
    #: :meta hide-value:
326
    noutputs = 1
9✔
327

328
    #: Transfer function numerator polynomial (array)
329
    #:
330
    #: The numerator of the transfer function is stored as an 2D list of
331
    #: arrays containing MIMO numerator coefficients, indexed by outputs and
332
    #: inputs.  For example, ``num[2][5]`` is the array of coefficients for
333
    #: the numerator of the transfer function from the sixth input to the
334
    #: third output.
335
    #:
336
    #: :meta hide-value:
337
    num = [[0]]
9✔
338

339
    #: Transfer function denominator polynomial (array)
340
    #:
341
    #: The numerator of the transfer function is store as an 2D list of
342
    #: arrays containing MIMO numerator coefficients, indexed by outputs and
343
    #: inputs.  For example, ``den[2][5]`` is the array of coefficients for
344
    #: the denominator of the transfer function from the sixth input to the
345
    #: third output.
346
    #:
347
    #: :meta hide-value:
348
    den = [[0]]
9✔
349

350
    def __call__(self, x, squeeze=None, warn_infinite=True):
9✔
351
        """Evaluate system's transfer function at complex frequencies.
352

353
        Returns the complex frequency response `sys(x)` where `x` is `s` for
354
        continuous-time systems and `z` for discrete-time systems.
355

356
        In general the system may be multiple input, multiple output
357
        (MIMO), where `m = self.ninputs` number of inputs and `p =
358
        self.noutputs` number of outputs.
359

360
        To evaluate at a frequency omega in radians per second, enter
361
        ``x = omega * 1j``, for continuous-time systems, or
362
        ``x = exp(1j * omega * dt)`` for discrete-time systems. Or use
363
        :meth:`TransferFunction.frequency_response`.
364

365
        Parameters
366
        ----------
367
        x : complex or complex 1D array_like
368
            Complex frequencies
369
        squeeze : bool, optional
370
            If squeeze=True, remove single-dimensional entries from the shape
371
            of the output even if the system is not SISO. If squeeze=False,
372
            keep all indices (output, input and, if omega is array_like,
373
            frequency) even if the system is SISO. The default value can be
374
            set using config.defaults['control.squeeze_frequency_response'].
375
            If True and the system is single-input single-output (SISO),
376
            return a 1D array rather than a 3D array.  Default value (True)
377
            set by config.defaults['control.squeeze_frequency_response'].
378
        warn_infinite : bool, optional
379
            If set to `False`, turn off divide by zero warning.
380

381
        Returns
382
        -------
383
        fresp : complex ndarray
384
            The frequency response of the system.  If the system is SISO and
385
            squeeze is not True, the shape of the array matches the shape of
386
            omega.  If the system is not SISO or squeeze is False, the first
387
            two dimensions of the array are indices for the output and input
388
            and the remaining dimensions match omega.  If ``squeeze`` is True
389
            then single-dimensional axes are removed.
390

391
        """
392
        out = self.horner(x, warn_infinite=warn_infinite)
9✔
393
        return _process_frequency_response(self, x, out, squeeze=squeeze)
9✔
394

395
    def horner(self, x, warn_infinite=True):
9✔
396
        """Evaluate system's transfer function at complex frequency
397
        using Horner's method.
398

399
        Evaluates `sys(x)` where `x` is `s` for continuous-time systems and `z`
400
        for discrete-time systems.
401

402
        Expects inputs and outputs to be formatted correctly. Use ``sys(x)``
403
        for a more user-friendly interface.
404

405
        Parameters
406
        ----------
407
        x : complex array_like or complex scalar
408
            Complex frequencies
409

410
        Returns
411
        -------
412
        output : (self.noutputs, self.ninputs, len(x)) complex ndarray
413
            Frequency response
414

415
        """
416
        # Make sure the argument is a 1D array of complex numbers
417
        x_arr = np.atleast_1d(x).astype(complex, copy=False)
9✔
418

419
        # Make sure that we are operating on a simple list
420
        if len(x_arr.shape) > 1:
9✔
421
            raise ValueError("input list must be 1D")
9✔
422

423
        # Initialize the output matrix in the proper shape
424
        out = empty((self.noutputs, self.ninputs, len(x_arr)), dtype=complex)
9✔
425

426
        # Set up error processing based on warn_infinite flag
427
        with np.errstate(all='warn' if warn_infinite else 'ignore'):
9✔
428
            for i in range(self.noutputs):
9✔
429
                for j in range(self.ninputs):
9✔
430
                    out[i][j] = (polyval(self.num[i][j], x_arr) /
9✔
431
                                 polyval(self.den[i][j], x_arr))
432
        return out
9✔
433

434
    def _truncatecoeff(self):
9✔
435
        """Remove extraneous zero coefficients from num and den.
436

437
        Check every element of the numerator and denominator matrices, and
438
        truncate leading zeros.  For instance, running self._truncatecoeff()
439
        will reduce self.num = [[[0, 0, 1, 2]]] to [[[1, 2]]].
440

441
        """
442

443
        # Beware: this is a shallow copy.  This should be okay.
444
        data = [self.num, self.den]
9✔
445
        for p in range(len(data)):
9✔
446
            for i in range(self.noutputs):
9✔
447
                for j in range(self.ninputs):
9✔
448
                    # Find the first nontrivial coefficient.
449
                    nonzero = None
9✔
450
                    for k in range(data[p][i][j].size):
9✔
451
                        if data[p][i][j][k]:
9✔
452
                            nonzero = k
9✔
453
                            break
9✔
454

455
                    if nonzero is None:
9✔
456
                        # The array is all zeros.
457
                        data[p][i][j] = zeros(1)
9✔
458
                    else:
459
                        # Truncate the trivial coefficients.
460
                        data[p][i][j] = data[p][i][j][nonzero:]
9✔
461
        [self.num, self.den] = data
9✔
462

463
    def __str__(self, var=None):
9✔
464
        """String representation of the transfer function.
465

466
        Based on the display_format property, the output will be formatted as
467
        either polynomials or in zpk form.
468
        """
469
        mimo = not self.issiso()
9✔
470
        if var is None:
9✔
471
            var = 's' if self.isctime() else 'z'
9✔
472
        outstr = f"{InputOutputSystem.__str__(self)}\n"
9✔
473

474
        for ni in range(self.ninputs):
9✔
475
            for no in range(self.noutputs):
9✔
476
                if mimo:
9✔
477
                    outstr += "\nInput %i to output %i:" % (ni + 1, no + 1)
9✔
478

479
                # Convert the numerator and denominator polynomials to strings.
480
                if self.display_format == 'poly':
9✔
481
                    numstr = _tf_polynomial_to_string(self.num[no][ni], var=var)
9✔
482
                    denstr = _tf_polynomial_to_string(self.den[no][ni], var=var)
9✔
483
                elif self.display_format == 'zpk':
9✔
484
                    num = self.num[no][ni]
9✔
485
                    if num.size == 1 and num.item() == 0:
9✔
486
                        # Catch a special case that SciPy doesn't handle
487
                        z, p, k = tf2zpk([1.], self.den[no][ni])
9✔
488
                        k = 0
9✔
489
                    else:
490
                        z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni])
9✔
491
                    numstr = _tf_factorized_polynomial_to_string(
9✔
492
                        z, gain=k, var=var)
493
                    denstr = _tf_factorized_polynomial_to_string(p, var=var)
9✔
494

495
                # Figure out the length of the separating line
496
                dashcount = max(len(numstr), len(denstr))
9✔
497
                dashes = '-' * dashcount
9✔
498

499
                # Center the numerator or denominator
500
                if len(numstr) < dashcount:
9✔
501
                    numstr = ' ' * ((dashcount - len(numstr)) // 2) + numstr
9✔
502
                if len(denstr) < dashcount:
9✔
503
                    denstr = ' ' * ((dashcount - len(denstr)) // 2) + denstr
9✔
504

505
                outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n"
9✔
506

507
        # If this is a strict discrete time system, print the sampling time
508
        if type(self.dt) != bool and self.isdtime(strict=True):
9✔
509
            outstr += "\ndt = " + str(self.dt) + "\n"
9✔
510

511
        return outstr
9✔
512

513
    # represent to implement a re-loadable version
514
    def __repr__(self):
9✔
515
        """Print transfer function in loadable form"""
516
        if self.issiso():
9✔
517
            return "TransferFunction({num}, {den}{dt})".format(
9✔
518
                num=self.num[0][0].__repr__(), den=self.den[0][0].__repr__(),
519
                dt=', {}'.format(self.dt) if isdtime(self, strict=True)
520
                else '')
521
        else:
522
            return "TransferFunction({num}, {den}{dt})".format(
9✔
523
                num=self.num.__repr__(), den=self.den.__repr__(),
524
                dt=', {}'.format(self.dt) if isdtime(self, strict=True)
525
                else '')
526

527
    def _repr_latex_(self, var=None):
9✔
528
        """LaTeX representation of transfer function, for Jupyter notebook"""
529

530
        mimo = not self.issiso()
9✔
531

532
        if var is None:
9✔
533
            var = 's' if self.isctime() else 'z'
9✔
534

535
        out = ['$$']
9✔
536

537
        if mimo:
9✔
538
            out.append(r"\begin{bmatrix}")
5✔
539

540
        for no in range(self.noutputs):
9✔
541
            for ni in range(self.ninputs):
9✔
542
                # Convert the numerator and denominator polynomials to strings.
543
                if self.display_format == 'poly':
9✔
544
                    numstr = _tf_polynomial_to_string(self.num[no][ni], var=var)
9✔
545
                    denstr = _tf_polynomial_to_string(self.den[no][ni], var=var)
9✔
546
                elif self.display_format == 'zpk':
×
547
                    z, p, k = tf2zpk(self.num[no][ni], self.den[no][ni])
×
548
                    numstr = _tf_factorized_polynomial_to_string(
×
549
                        z, gain=k, var=var)
550
                    denstr = _tf_factorized_polynomial_to_string(p, var=var)
×
551

552
                numstr = _tf_string_to_latex(numstr, var=var)
9✔
553
                denstr = _tf_string_to_latex(denstr, var=var)
9✔
554

555
                out += [r"\frac{", numstr, "}{", denstr, "}"]
9✔
556

557
                if mimo and ni < self.ninputs - 1:
9✔
558
                    out.append("&")
5✔
559

560
            if mimo:
9✔
561
                out.append(r"\\")
5✔
562

563
        if mimo:
9✔
564
            out.append(r" \end{bmatrix}")
5✔
565

566
        # See if this is a discrete time system with specific sampling time
567
        if not (self.dt is None) and type(self.dt) != bool and self.dt > 0:
9✔
568
            out += [r"\quad dt = ", str(self.dt)]
9✔
569

570
        out.append("$$")
9✔
571

572
        return ''.join(out)
9✔
573

574
    def __neg__(self):
9✔
575
        """Negate a transfer function."""
576
        num = deepcopy(self.num)
9✔
577
        for i in range(self.noutputs):
9✔
578
            for j in range(self.ninputs):
9✔
579
                num[i][j] *= -1
9✔
580
        return TransferFunction(num, self.den, self.dt)
9✔
581

582
    def __add__(self, other):
9✔
583
        """Add two LTI objects (parallel connection)."""
584
        from .statesp import StateSpace
9✔
585

586
        # Convert the second argument to a transfer function.
587
        if isinstance(other, StateSpace):
9✔
588
            other = _convert_to_transfer_function(other)
9✔
589
        elif isinstance(other, (int, float, complex, np.number, np.ndarray)):
9✔
590
            other = _convert_to_transfer_function(other, inputs=self.ninputs,
9✔
591
                                                  outputs=self.noutputs)
592

593
        if not isinstance(other, TransferFunction):
9✔
594
            return NotImplemented
9✔
595

596
        # Check that the input-output sizes are consistent.
597
        if self.ninputs != other.ninputs:
9✔
598
            raise ValueError(
5✔
599
                "The first summand has %i input(s), but the second has %i."
600
                % (self.ninputs, other.ninputs))
601
        if self.noutputs != other.noutputs:
9✔
602
            raise ValueError(
9✔
603
                "The first summand has %i output(s), but the second has %i."
604
                % (self.noutputs, other.noutputs))
605

606
        dt = common_timebase(self.dt, other.dt)
9✔
607

608
        # Preallocate the numerator and denominator of the sum.
609
        num = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)]
9✔
610
        den = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)]
9✔
611

612
        for i in range(self.noutputs):
9✔
613
            for j in range(self.ninputs):
9✔
614
                num[i][j], den[i][j] = _add_siso(
9✔
615
                    self.num[i][j], self.den[i][j],
616
                    other.num[i][j], other.den[i][j])
617

618
        return TransferFunction(num, den, dt)
9✔
619

620
    def __radd__(self, other):
9✔
621
        """Right add two LTI objects (parallel connection)."""
622
        return self + other
9✔
623

624
    def __sub__(self, other):
9✔
625
        """Subtract two LTI objects."""
626
        return self + (-other)
9✔
627

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

632
    def __mul__(self, other):
9✔
633
        """Multiply two LTI objects (serial connection)."""
634
        from .statesp import StateSpace
9✔
635

636
        # Convert the second argument to a transfer function.
637
        if isinstance(other, (StateSpace, np.ndarray)):
9✔
638
            other = _convert_to_transfer_function(other)
9✔
639
        elif isinstance(other, (int, float, complex, np.number)):
9✔
640
            # Multiply by a scaled identify matrix (transfer function)
641
            other = _convert_to_transfer_function(np.eye(self.ninputs) * other)
9✔
642
        if not isinstance(other, TransferFunction):
9✔
643
            return NotImplemented
9✔
644

645
        # Check that the input-output sizes are consistent.
646
        if self.ninputs != other.noutputs:
9✔
647
            raise ValueError(
9✔
648
                "C = A * B: A has %i column(s) (input(s)), but B has %i "
649
                "row(s)\n(output(s))." % (self.ninputs, other.noutputs))
650

651
        inputs = other.ninputs
9✔
652
        outputs = self.noutputs
9✔
653

654
        dt = common_timebase(self.dt, other.dt)
9✔
655

656
        # Preallocate the numerator and denominator of the sum.
657
        num = [[[0] for j in range(inputs)] for i in range(outputs)]
9✔
658
        den = [[[1] for j in range(inputs)] for i in range(outputs)]
9✔
659

660
        # Temporary storage for the summands needed to find the (i, j)th
661
        # element of the product.
662
        num_summand = [[] for k in range(self.ninputs)]
9✔
663
        den_summand = [[] for k in range(self.ninputs)]
9✔
664

665
        # Multiply & add.
666
        for row in range(outputs):
9✔
667
            for col in range(inputs):
9✔
668
                for k in range(self.ninputs):
9✔
669
                    num_summand[k] = polymul(
9✔
670
                        self.num[row][k], other.num[k][col])
671
                    den_summand[k] = polymul(
9✔
672
                        self.den[row][k], other.den[k][col])
673
                    num[row][col], den[row][col] = _add_siso(
9✔
674
                        num[row][col], den[row][col],
675
                        num_summand[k], den_summand[k])
676

677
        return TransferFunction(num, den, dt)
9✔
678

679
    def __rmul__(self, other):
9✔
680
        """Right multiply two LTI objects (serial connection)."""
681

682
        # Convert the second argument to a transfer function.
683
        if isinstance(other, (int, float, complex, np.number)):
9✔
684
            # Multiply by a scaled identify matrix (transfer function)
685
            other = _convert_to_transfer_function(np.eye(self.noutputs) * other)
9✔
686
        else:
687
            other = _convert_to_transfer_function(other)
9✔
688

689
        # Check that the input-output sizes are consistent.
690
        if other.ninputs != self.noutputs:
9✔
691
            raise ValueError(
9✔
692
                "C = A * B: A has %i column(s) (input(s)), but B has %i "
693
                "row(s)\n(output(s))." % (other.ninputs, self.noutputs))
694

695
        inputs = self.ninputs
9✔
696
        outputs = other.noutputs
9✔
697

698
        dt = common_timebase(self.dt, other.dt)
9✔
699

700
        # Preallocate the numerator and denominator of the sum.
701
        num = [[[0] for j in range(inputs)] for i in range(outputs)]
9✔
702
        den = [[[1] for j in range(inputs)] for i in range(outputs)]
9✔
703

704
        # Temporary storage for the summands needed to find the
705
        # (i, j)th element
706
        # of the product.
707
        num_summand = [[] for k in range(other.ninputs)]
9✔
708
        den_summand = [[] for k in range(other.ninputs)]
9✔
709

710
        for i in range(outputs):  # Iterate through rows of product.
9✔
711
            for j in range(inputs):  # Iterate through columns of product.
9✔
712
                for k in range(other.ninputs):  # Multiply & add.
9✔
713
                    num_summand[k] = polymul(other.num[i][k], self.num[k][j])
9✔
714
                    den_summand[k] = polymul(other.den[i][k], self.den[k][j])
9✔
715
                    num[i][j], den[i][j] = _add_siso(
9✔
716
                        num[i][j], den[i][j],
717
                        num_summand[k], den_summand[k])
718

719
        return TransferFunction(num, den, dt)
9✔
720

721
    # TODO: Division of MIMO transfer function objects is not written yet.
722
    def __truediv__(self, other):
9✔
723
        """Divide two LTI objects."""
724

725
        if isinstance(other, (int, float, complex, np.number)):
9✔
726
            # Multiply by a scaled identify matrix (transfer function)
727
            other = _convert_to_transfer_function(np.eye(self.ninputs) * other)
9✔
728
        else:
729
            other = _convert_to_transfer_function(other)
9✔
730

731
        if (self.ninputs > 1 or self.noutputs > 1 or
9✔
732
                other.ninputs > 1 or other.noutputs > 1):
733
            raise NotImplementedError(
734
                "TransferFunction.__truediv__ is currently \
735
                implemented only for SISO systems.")
736

737
        dt = common_timebase(self.dt, other.dt)
9✔
738

739
        num = polymul(self.num[0][0], other.den[0][0])
9✔
740
        den = polymul(self.den[0][0], other.num[0][0])
9✔
741

742
        return TransferFunction(num, den, dt)
9✔
743

744
    # TODO: Division of MIMO transfer function objects is not written yet.
745
    def __rtruediv__(self, other):
9✔
746
        """Right divide two LTI objects."""
747
        if isinstance(other, (int, float, complex, np.number)):
9✔
748
            other = _convert_to_transfer_function(
9✔
749
                other, inputs=self.ninputs,
750
                outputs=self.ninputs)
751
        else:
752
            other = _convert_to_transfer_function(other)
9✔
753

754
        if (self.ninputs > 1 or self.noutputs > 1 or
9✔
755
                other.ninputs > 1 or other.noutputs > 1):
756
            raise NotImplementedError(
757
                "TransferFunction.__rtruediv__ is currently implemented only "
758
                "for SISO systems.")
759

760
        return other / self
9✔
761

762
    def __pow__(self, other):
9✔
763
        if not type(other) == int:
9✔
764
            raise ValueError("Exponent must be an integer")
9✔
765
        if other == 0:
9✔
766
            return TransferFunction([1], [1])  # unity
9✔
767
        if other > 0:
9✔
768
            return self * (self**(other - 1))
9✔
769
        if other < 0:
9✔
770
            return (TransferFunction([1], [1]) / self) * (self**(other + 1))
9✔
771

772
    def __getitem__(self, key):
9✔
773
        if not isinstance(key, Iterable) or len(key) != 2:
9✔
774
            raise IOError(
×
775
                "must provide indices of length 2 for transfer functions")
776

777
        # Convert signal names to integer offsets (via NamedSignal object)
778
        iomap = NamedSignal(
9✔
779
            np.empty((self.noutputs, self.ninputs)),
780
            self.output_labels, self.input_labels)
781
        indices = iomap._parse_key(key, level=1)  # ignore index checks
9✔
782
        outdx, outputs = _process_subsys_index(
9✔
783
            indices[0], self.output_labels, slice_to_list=True)
784
        inpdx, inputs = _process_subsys_index(
9✔
785
            indices[1], self.input_labels, slice_to_list=True)
786
        
787
        # Construct the transfer function for the subsyste
788
        num, den = [], []
9✔
789
        for i in outdx:
9✔
790
            num_i = []
9✔
791
            den_i = []
9✔
792
            for j in inpdx:
9✔
793
                num_i.append(self.num[i][j])
9✔
794
                den_i.append(self.den[i][j])
9✔
795
            num.append(num_i)
9✔
796
            den.append(den_i)
9✔
797

798
        # Create the system name
799
        sysname = config.defaults['iosys.indexed_system_name_prefix'] + \
9✔
800
            self.name + config.defaults['iosys.indexed_system_name_suffix']
801

802
        return TransferFunction(
9✔
803
            num, den, self.dt, inputs=inputs, outputs=outputs, name=sysname)
804

805
    def freqresp(self, omega):
9✔
806
        """Evaluate transfer function at complex frequencies.
807

808
        .. deprecated::0.9.0
809
            Method has been given the more pythonic name
810
            :meth:`TransferFunction.frequency_response`. Or use
811
            :func:`freqresp` in the MATLAB compatibility module.
812
        """
813
        warn("TransferFunction.freqresp(omega) will be removed in a "
9✔
814
             "future release of python-control; use "
815
             "sys.frequency_response(omega), or freqresp(sys, omega) in the "
816
             "MATLAB compatibility module instead", FutureWarning)
817
        return self.frequency_response(omega)
9✔
818

819
    def poles(self):
9✔
820
        """Compute the poles of a transfer function."""
821
        _, den, denorder = self._common_den(allow_nonproper=True)
9✔
822
        rts = []
9✔
823
        for d, o in zip(den, denorder):
9✔
824
            rts.extend(roots(d[:o + 1]))
9✔
825
        return np.array(rts).astype(complex)
9✔
826

827
    def zeros(self):
9✔
828
        """Compute the zeros of a transfer function."""
829
        if self.ninputs > 1 or self.noutputs > 1:
9✔
830
            raise NotImplementedError(
831
                "TransferFunction.zeros is currently only implemented "
832
                "for SISO systems.")
833
        else:
834
            # for now, just give zeros of a SISO tf
835
            return roots(self.num[0][0]).astype(complex)
9✔
836

837
    def feedback(self, other=1, sign=-1):
9✔
838
        """Feedback interconnection between two LTI objects."""
839
        other = _convert_to_transfer_function(other)
9✔
840

841
        if (self.ninputs > 1 or self.noutputs > 1 or
9✔
842
                other.ninputs > 1 or other.noutputs > 1):
843
            # TODO: MIMO feedback
844
            raise ControlMIMONotImplemented(
5✔
845
                "TransferFunction.feedback is currently not implemented for "
846
                "MIMO systems.")
847
        dt = common_timebase(self.dt, other.dt)
9✔
848

849
        num1 = self.num[0][0]
9✔
850
        den1 = self.den[0][0]
9✔
851
        num2 = other.num[0][0]
9✔
852
        den2 = other.den[0][0]
9✔
853

854
        num = polymul(num1, den2)
9✔
855
        den = polyadd(polymul(den2, den1), -sign * polymul(num2, num1))
9✔
856

857
        return TransferFunction(num, den, dt)
9✔
858

859
        # For MIMO or SISO systems, the analytic expression is
860
        #     self / (1 - sign * other * self)
861
        # But this does not work correctly because the state size will be too
862
        # large.
863

864
    def minreal(self, tol=None):
9✔
865
        """Remove cancelling pole/zero pairs from a transfer function"""
866
        # based on octave minreal
867

868
        # default accuracy
869
        from sys import float_info
9✔
870
        sqrt_eps = sqrt(float_info.epsilon)
9✔
871

872
        # pre-allocate arrays
873
        num = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)]
9✔
874
        den = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)]
9✔
875

876
        for i in range(self.noutputs):
9✔
877
            for j in range(self.ninputs):
9✔
878

879
                # split up in zeros, poles and gain
880
                newzeros = []
9✔
881
                zeros = roots(self.num[i][j])
9✔
882
                poles = roots(self.den[i][j])
9✔
883
                gain = self.num[i][j][0] / self.den[i][j][0]
9✔
884

885
                # check all zeros
886
                for z in zeros:
9✔
887
                    t = tol or \
9✔
888
                        1000 * max(float_info.epsilon, abs(z) * sqrt_eps)
889
                    idx = where(abs(z - poles) < t)[0]
9✔
890
                    if len(idx):
9✔
891
                        # cancel this zero against one of the poles
892
                        poles = delete(poles, idx[0])
9✔
893
                    else:
894
                        # keep this zero
895
                        newzeros.append(z)
9✔
896

897
                # poly([]) returns a scalar, but we always want a 1d array
898
                num[i][j] = np.atleast_1d(gain * real(poly(newzeros)))
9✔
899
                den[i][j] = np.atleast_1d(real(poly(poles)))
9✔
900

901
        # end result
902
        return TransferFunction(num, den, self.dt)
9✔
903

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

907
        For instance,
908

909
        >>> out = tfobject.returnScipySignalLTI()               # doctest: +SKIP
910
        >>> out[3][5]                                           # doctest: +SKIP
911

912
        is a :class:`scipy.signal.lti` object corresponding to the
913
        transfer function from the 6th input to the 4th output.
914

915
        Parameters
916
        ----------
917
        strict : bool, optional
918
            True (default):
919
                The timebase `tfobject.dt` cannot be None; it must be
920
                continuous (0) or discrete (True or > 0).
921
            False:
922
                if `tfobject.dt` is None, continuous time
923
                :class:`scipy.signal.lti` objects are returned
924

925
        Returns
926
        -------
927
        out : list of list of :class:`scipy.signal.TransferFunction`
928
            continuous time (inheriting from :class:`scipy.signal.lti`)
929
            or discrete time (inheriting from :class:`scipy.signal.dlti`)
930
            SISO objects
931
        """
932
        if strict and self.dt is None:
9✔
933
            raise ValueError("with strict=True, dt cannot be None")
9✔
934

935
        if self.dt:
9✔
936
            kwdt = {'dt': self.dt}
9✔
937
        else:
938
            # scipy convention for continuous time lti systems: call without
939
            # dt keyword argument
940
            kwdt = {}
9✔
941

942
        # Preallocate the output.
943
        out = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)]
9✔
944

945
        for i in range(self.noutputs):
9✔
946
            for j in range(self.ninputs):
9✔
947
                out[i][j] = signalTransferFunction(self.num[i][j],
9✔
948
                                                   self.den[i][j],
949
                                                   **kwdt)
950

951
        return out
9✔
952

953
    def _common_den(self, imag_tol=None, allow_nonproper=False):
9✔
954
        """
955
        Compute MIMO common denominators; return them and adjusted numerators.
956

957
        This function computes the denominators per input containing all
958
        the poles of sys.den, and reports it as the array den.  The
959
        output numerator array num is modified to use the common
960
        denominator for this input/column; the coefficient arrays are also
961
        padded with zeros to be the same size for all num/den.
962

963
        Parameters
964
        ----------
965
        imag_tol: float
966
            Threshold for the imaginary part of a root to use in detecting
967
            complex poles
968

969
        allow_nonproper : boolean
970
            Do not enforce proper transfer functions
971

972
        Returns
973
        -------
974
        num: array
975
            n by n by kd where n = max(sys.noutputs,sys.ninputs)
976
                              kd = max(denorder)+1
977
            Multi-dimensional array of numerator coefficients. num[i,j]
978
            gives the numerator coefficient array for the ith output and jth
979
            input; padded for use in td04ad ('C' option); matches the
980
            denorder order; highest coefficient starts on the left.
981
            If allow_nonproper=True and the order of a numerator exceeds the
982
            order of the common denominator, num will be returned as None
983

984
        den: array
985
            sys.ninputs by kd
986
            Multi-dimensional array of coefficients for common denominator
987
            polynomial, one row per input. The array is prepared for use in
988
            slycot td04ad, the first element is the highest-order polynomial
989
            coefficient of s, matching the order in denorder. If denorder <
990
            number of columns in den, the den is padded with zeros.
991

992
        denorder: array of int, orders of den, one per input
993

994

995

996
        Examples
997
        --------
998
        >>> num, den, denorder = sys._common_den()              # doctest: +SKIP
999

1000
        """
1001

1002
        # Machine precision for floats.
1003
        eps = finfo(float).eps
9✔
1004
        real_tol = sqrt(eps * self.ninputs * self.noutputs)
9✔
1005

1006
        # The tolerance to use in deciding if a pole is complex
1007
        if (imag_tol is None):
9✔
1008
            imag_tol = 2 * real_tol
9✔
1009

1010
        # A list to keep track of cumulative poles found as we scan
1011
        # self.den[..][..]
1012
        poles = [[] for j in range(self.ninputs)]
9✔
1013

1014
        # RvP, new implementation 180526, issue #194
1015
        # BG, modification, issue #343, PR #354
1016

1017
        # pre-calculate the poles for all num, den
1018
        # has zeros, poles, gain, list for pole indices not in den,
1019
        # number of poles known at the time analyzed
1020

1021
        # do not calculate minreal. Rory's hint .minreal()
1022
        poleset = []
9✔
1023
        for i in range(self.noutputs):
9✔
1024
            poleset.append([])
9✔
1025
            for j in range(self.ninputs):
9✔
1026
                if abs(self.num[i][j]).max() <= eps:
9✔
1027
                    poleset[-1].append([array([], dtype=float),
5✔
1028
                                        roots(self.den[i][j]), 0.0, [], 0])
1029
                else:
1030
                    z, p, k = tf2zpk(self.num[i][j], self.den[i][j])
9✔
1031
                    poleset[-1].append([z, p, k, [], 0])
9✔
1032

1033
        # collect all individual poles
1034
        for j in range(self.ninputs):
9✔
1035
            for i in range(self.noutputs):
9✔
1036
                currentpoles = poleset[i][j][1]
9✔
1037
                nothave = ones(currentpoles.shape, dtype=bool)
9✔
1038
                for ip, p in enumerate(poles[j]):
9✔
1039
                    collect = (np.isclose(currentpoles.real, p.real,
9✔
1040
                                          atol=real_tol) &
1041
                               np.isclose(currentpoles.imag, p.imag,
1042
                                          atol=imag_tol) &
1043
                               nothave)
1044
                    if np.any(collect):
9✔
1045
                        # mark first found pole as already collected
1046
                        nothave[nonzero(collect)[0][0]] = False
9✔
1047
                    else:
1048
                        # remember id of pole not in tf
1049
                        poleset[i][j][3].append(ip)
9✔
1050
                for h, c in zip(nothave, currentpoles):
9✔
1051
                    if h:
9✔
1052
                        if abs(c.imag) < imag_tol:
9✔
1053
                            c = c.real
9✔
1054
                        poles[j].append(c)
9✔
1055
                # remember how many poles now known
1056
                poleset[i][j][4] = len(poles[j])
9✔
1057

1058
        # figure out maximum number of poles, for sizing the den
1059
        maxindex = max([len(p) for p in poles])
9✔
1060
        den = zeros((self.ninputs, maxindex + 1), dtype=float)
9✔
1061
        num = zeros((max(1, self.noutputs, self.ninputs),
9✔
1062
                     max(1, self.noutputs, self.ninputs),
1063
                     maxindex + 1),
1064
                    dtype=float)
1065
        denorder = zeros((self.ninputs,), dtype=int)
9✔
1066

1067
        havenonproper = False
9✔
1068

1069
        for j in range(self.ninputs):
9✔
1070
            if not len(poles[j]):
9✔
1071
                # no poles matching this input; only one or more gains
1072
                den[j, 0] = 1.0
9✔
1073
                for i in range(self.noutputs):
9✔
1074
                    num[i, j, 0] = poleset[i][j][2]
9✔
1075
            else:
1076
                # create the denominator matching this input
1077
                # coefficients should be padded on right, ending at maxindex
1078
                maxindex = len(poles[j])
9✔
1079
                den[j, :maxindex+1] = poly(poles[j])
9✔
1080
                denorder[j] = maxindex
9✔
1081

1082
                # now create the numerator, also padded on the right
1083
                for i in range(self.noutputs):
9✔
1084
                    # start with the current set of zeros for this output
1085
                    nwzeros = list(poleset[i][j][0])
9✔
1086
                    # add all poles not found in the original denominator,
1087
                    # and the ones later added from other denominators
1088
                    for ip in chain(poleset[i][j][3],
9✔
1089
                                    range(poleset[i][j][4], maxindex)):
1090
                        nwzeros.append(poles[j][ip])
9✔
1091

1092
                    numpoly = poleset[i][j][2] * np.atleast_1d(poly(nwzeros))
9✔
1093

1094
                    # td04ad expects a proper transfer function. If the
1095
                    # numerater has a higher order than the denominator, the
1096
                    # padding will fail
1097
                    if len(numpoly) > maxindex + 1:
9✔
1098
                        if allow_nonproper:
9✔
1099
                            havenonproper = True
9✔
1100
                            break
9✔
1101
                        raise ValueError(
9✔
1102
                            self.__str__() +
1103
                            "is not a proper transfer function. "
1104
                            "The degree of the numerators must not exceed "
1105
                            "the degree of the denominators.")
1106

1107
                    # numerator polynomial should be padded on left and right
1108
                    #   ending at maxindex to line up with what td04ad expects.
1109
                    num[i, j, maxindex+1-len(numpoly):maxindex+1] = numpoly
9✔
1110
                    # print(num[i, j])
1111

1112
        if havenonproper:
9✔
1113
            num = None
9✔
1114

1115
        return num, den, denorder
9✔
1116

1117
    def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
9✔
1118
               name=None, copy_names=True, **kwargs):
1119
        """Convert a continuous-time system to discrete time
1120

1121
        Creates a discrete-time system from a continuous-time system by
1122
        sampling.  Multiple methods of conversion are supported.
1123

1124
        Parameters
1125
        ----------
1126
        Ts : float
1127
            Sampling period
1128
        method : {"gbt", "bilinear", "euler", "backward_diff",
1129
                  "zoh", "matched"}
1130
            Method to use for sampling:
1131

1132
            * gbt: generalized bilinear transformation
1133
            * bilinear or tustin: Tustin's approximation ("gbt" with alpha=0.5)
1134
            * euler: Euler (or forward difference) method ("gbt" with alpha=0)
1135
            * backward_diff: Backwards difference ("gbt" with alpha=1.0)
1136
            * zoh: zero-order hold (default)
1137
        alpha : float within [0, 1]
1138
            The generalized bilinear transformation weighting parameter, which
1139
            should only be specified with method="gbt", and is ignored
1140
            otherwise. See :func:`scipy.signal.cont2discrete`.
1141
        prewarp_frequency : float within [0, infinity)
1142
            The frequency [rad/s] at which to match with the input continuous-
1143
            time system's magnitude and phase (the gain=1 crossover frequency,
1144
            for example). Should only be specified with method='bilinear' or
1145
            'gbt' with alpha=0.5 and ignored otherwise.
1146
        name : string, optional
1147
            Set the name of the sampled system.  If not specified and
1148
            if `copy_names` is `False`, a generic name <sys[id]> is generated
1149
            with a unique integer id.  If `copy_names` is `True`, the new system
1150
            name is determined by adding the prefix and suffix strings in
1151
            config.defaults['iosys.sampled_system_name_prefix'] and
1152
            config.defaults['iosys.sampled_system_name_suffix'], with the
1153
            default being to add the suffix '$sampled'.
1154
        copy_names : bool, Optional
1155
            If True, copy the names of the input signals, output
1156
            signals, and states to the sampled system.
1157

1158
        Returns
1159
        -------
1160
        sysd : TransferFunction system
1161
            Discrete-time system, with sample period Ts
1162

1163
        Other Parameters
1164
        ----------------
1165
        inputs : int, list of str or None, optional
1166
            Description of the system inputs.  If not specified, the origional
1167
            system inputs are used.  See :class:`InputOutputSystem` for more
1168
            information.
1169
        outputs : int, list of str or None, optional
1170
            Description of the system outputs.  Same format as `inputs`.
1171

1172
        Notes
1173
        -----
1174
        1. Available only for SISO systems
1175

1176
        2. Uses :func:`scipy.signal.cont2discrete`
1177

1178
        Examples
1179
        --------
1180
        >>> sys = ct.tf(1, [1, 1])
1181
        >>> sysd = sys.sample(0.5, method='bilinear')
1182

1183
        """
1184
        if not self.isctime():
9✔
1185
            raise ValueError("System must be continuous time system")
×
1186
        if not self.issiso():
9✔
1187
            raise ControlMIMONotImplemented("Not implemented for MIMO systems")
×
1188
        if method == "matched":
9✔
1189
            if prewarp_frequency is not None:
9✔
1190
                warn('prewarp_frequency ignored: incompatible conversion')
×
1191
            return _c2d_matched(self, Ts, name=name, **kwargs)
9✔
1192
        sys = (self.num[0][0], self.den[0][0])
9✔
1193
        if prewarp_frequency is not None:
9✔
1194
            if method in ('bilinear', 'tustin') or \
9✔
1195
                    (method == 'gbt' and alpha == 0.5):
1196
                Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency
9✔
1197
            else:
1198
                warn('prewarp_frequency ignored: incompatible conversion')
9✔
1199
                Twarp = Ts
9✔
1200
        else:
1201
            Twarp = Ts
9✔
1202
        numd, dend, _ = cont2discrete(sys, Twarp, method, alpha)
9✔
1203

1204
        sysd = TransferFunction(numd[0, :], dend, Ts)
9✔
1205
        # copy over the system name, inputs, outputs, and states
1206
        if copy_names:
9✔
1207
            sysd._copy_names(self, prefix_suffix_name='sampled')
9✔
1208
            if name is not None:
9✔
1209
                sysd.name = name
9✔
1210
        # pass desired signal names if names were provided
1211
        return TransferFunction(sysd, name=name, **kwargs)
9✔
1212

1213
    def dcgain(self, warn_infinite=False):
9✔
1214
        """Return the zero-frequency (or DC) gain.
1215

1216
        For a continous-time transfer function G(s), the DC gain is G(0)
1217
        For a discrete-time transfer function G(z), the DC gain is G(1)
1218

1219
        Parameters
1220
        ----------
1221
        warn_infinite : bool, optional
1222
            By default, don't issue a warning message if the zero-frequency
1223
            gain is infinite.  Setting `warn_infinite` to generate the warning
1224
            message.
1225

1226
        Returns
1227
        -------
1228
        gain : (noutputs, ninputs) ndarray or scalar
1229
            Array or scalar value for SISO systems, depending on
1230
            config.defaults['control.squeeze_frequency_response'].
1231
            The value of the array elements or the scalar is either the
1232
            zero-frequency (or DC) gain, or `inf`, if the frequency response
1233
            is singular.
1234

1235
            For real valued systems, the empty imaginary part of the
1236
            complex zero-frequency response is discarded and a real array or
1237
            scalar is returned.
1238

1239
        Examples
1240
        --------
1241
        >>> G = ct.tf([1], [1, 4])
1242
        >>> G.dcgain()
1243
        np.float64(0.25)
1244

1245
        """
1246
        return self._dcgain(warn_infinite)
9✔
1247

1248
    def _isstatic(self):
9✔
1249
        """returns True if and only if all of the numerator and denominator
1250
        polynomials of the (possibly MIMO) transfer function are zeroth order,
1251
        that is, if the system has no dynamics. """
1252
        for list_of_polys in self.num, self.den:
9✔
1253
            for row in list_of_polys:
9✔
1254
                for poly in row:
9✔
1255
                    if len(poly) > 1:
9✔
1256
                        return False
9✔
1257
        return True
9✔
1258

1259
    # Attributes for differentiation and delay
1260
    #
1261
    # These attributes are created here with sphinx docstrings so that the
1262
    # autodoc generated documentation has a description.  The actual values of
1263
    # the class attributes are set at the bottom of the file to avoid problems
1264
    # with recursive calls.
1265

1266
    #: Differentation operator (continuous time)
1267
    #:
1268
    #: The ``s`` constant can be used to create continuous time transfer
1269
    #: functions using algebraic expressions.
1270
    #:
1271
    #: Example
1272
    #: -------
1273
    #: >>> s = TransferFunction.s                               # doctest: +SKIP
1274
    #: >>> G  = (s + 1)/(s**2 + 2*s + 1)                        # doctest: +SKIP
1275
    #:
1276
    #: :meta hide-value:
1277
    s = None
9✔
1278

1279
    #: Delay operator (discrete time)
1280
    #:
1281
    #: The ``z`` constant can be used to create discrete time transfer
1282
    #: functions using algebraic expressions.
1283
    #:
1284
    #: Example
1285
    #: -------
1286
    #: >>> z = TransferFunction.z                               # doctest: +SKIP
1287
    #: >>> G  = 2 * z / (4 * z**3 + 3*z - 1)                    # doctest: +SKIP
1288
    #:
1289
    #: :meta hide-value:
1290
    z = None
9✔
1291

1292

1293
# c2d function contributed by Benjamin White, Oct 2012
1294
def _c2d_matched(sysC, Ts, **kwargs):
9✔
1295
    if not sysC.issiso():
9✔
1296
        raise ControlMIMONotImplemented("Not implemented for MIMO systems")
×
1297

1298
    # Pole-zero match method of continuous to discrete time conversion
1299
    szeros, spoles, _ = tf2zpk(sysC.num[0][0], sysC.den[0][0])
9✔
1300
    zzeros = [0] * len(szeros)
9✔
1301
    zpoles = [0] * len(spoles)
9✔
1302
    pregainnum = [0] * len(szeros)
9✔
1303
    pregainden = [0] * len(spoles)
9✔
1304
    for idx, s in enumerate(szeros):
9✔
1305
        sTs = s * Ts
9✔
1306
        z = exp(sTs)
9✔
1307
        zzeros[idx] = z
9✔
1308
        pregainnum[idx] = 1 - z
9✔
1309
    for idx, s in enumerate(spoles):
9✔
1310
        sTs = s * Ts
9✔
1311
        z = exp(sTs)
9✔
1312
        zpoles[idx] = z
9✔
1313
        pregainden[idx] = 1 - z
9✔
1314
    zgain = np.multiply.reduce(pregainnum) / np.multiply.reduce(pregainden)
9✔
1315
    gain = sysC.dcgain() / zgain
9✔
1316
    sysDnum, sysDden = zpk2tf(zzeros, zpoles, gain)
9✔
1317
    return TransferFunction(sysDnum, sysDden, Ts, **kwargs)
9✔
1318

1319

1320
# Utility function to convert a transfer function polynomial to a string
1321
# Borrowed from poly1d library
1322
def _tf_polynomial_to_string(coeffs, var='s'):
9✔
1323
    """Convert a transfer function polynomial to a string"""
1324

1325
    thestr = "0"
9✔
1326

1327
    # Compute the number of coefficients
1328
    N = len(coeffs) - 1
9✔
1329

1330
    for k in range(len(coeffs)):
9✔
1331
        coefstr = _float2str(abs(coeffs[k]))
9✔
1332
        power = (N - k)
9✔
1333
        if power == 0:
9✔
1334
            if coefstr != '0':
9✔
1335
                newstr = '%s' % (coefstr,)
9✔
1336
            else:
1337
                if k == 0:
9✔
1338
                    newstr = '0'
9✔
1339
                else:
1340
                    newstr = ''
9✔
1341
        elif power == 1:
9✔
1342
            if coefstr == '0':
9✔
1343
                newstr = ''
9✔
1344
            elif coefstr == '1':
9✔
1345
                newstr = var
9✔
1346
            else:
1347
                newstr = '%s %s' % (coefstr, var)
9✔
1348
        else:
1349
            if coefstr == '0':
9✔
1350
                newstr = ''
9✔
1351
            elif coefstr == '1':
9✔
1352
                newstr = '%s^%d' % (var, power,)
9✔
1353
            else:
1354
                newstr = '%s %s^%d' % (coefstr, var, power)
9✔
1355

1356
        if k > 0:
9✔
1357
            if newstr != '':
9✔
1358
                if coeffs[k] < 0:
9✔
1359
                    thestr = "%s - %s" % (thestr, newstr)
9✔
1360
                else:
1361
                    thestr = "%s + %s" % (thestr, newstr)
9✔
1362
        elif (k == 0) and (newstr != '') and (coeffs[k] < 0):
9✔
1363
            thestr = "-%s" % (newstr,)
9✔
1364
        else:
1365
            thestr = newstr
9✔
1366
    return thestr
9✔
1367

1368

1369
def _tf_factorized_polynomial_to_string(roots, gain=1, var='s'):
9✔
1370
    """Convert a factorized polynomial to a string"""
1371

1372
    if roots.size == 0:
9✔
1373
        return _float2str(gain)
9✔
1374

1375
    factors = []
9✔
1376
    for root in sorted(roots, reverse=True):
9✔
1377
        if np.isreal(root):
9✔
1378
            if root == 0:
9✔
1379
                factor = f"{var}"
9✔
1380
                factors.append(factor)
9✔
1381
            elif root > 0:
9✔
1382
                factor = f"{var} - {_float2str(np.abs(root))}"
9✔
1383
                factors.append(factor)
9✔
1384
            else:
1385
                factor = f"{var} + {_float2str(np.abs(root))}"
9✔
1386
                factors.append(factor)
9✔
1387
        elif np.isreal(root * 1j):
9✔
1388
            if root.imag > 0:
9✔
1389
                factor = f"{var} - {_float2str(np.abs(root))}j"
9✔
1390
                factors.append(factor)
9✔
1391
            else:
1392
                factor = f"{var} + {_float2str(np.abs(root))}j"
9✔
1393
                factors.append(factor)
9✔
1394
        else:
1395
            if root.real > 0:
9✔
1396
                factor = f"{var} - ({_float2str(root)})"
9✔
1397
                factors.append(factor)
9✔
1398
            else:
1399
                factor = f"{var} + ({_float2str(-root)})"
9✔
1400
                factors.append(factor)
9✔
1401

1402
    multiplier = ''
9✔
1403
    if round(gain, 4) != 1.0:
9✔
1404
        multiplier = _float2str(gain) + " "
9✔
1405

1406
    if len(factors) > 1 or multiplier:
9✔
1407
        factors = [f"({factor})" for factor in factors]
9✔
1408

1409
    return multiplier + " ".join(factors)
9✔
1410

1411

1412
def _tf_string_to_latex(thestr, var='s'):
9✔
1413
    """ make sure to superscript all digits in a polynomial string
1414
        and convert float coefficients in scientific notation
1415
        to prettier LaTeX representation """
1416
    # TODO: make the multiplication sign configurable
1417
    expmul = r' \\times'
9✔
1418
    thestr = sub(var + r'\^(\d{2,})', var + r'^{\1}', thestr)
9✔
1419
    thestr = sub(r'[eE]\+0*(\d+)', expmul + r' 10^{\1}', thestr)
9✔
1420
    thestr = sub(r'[eE]\-0*(\d+)', expmul + r' 10^{-\1}', thestr)
9✔
1421
    return thestr
9✔
1422

1423

1424
def _add_siso(num1, den1, num2, den2):
9✔
1425
    """Return num/den = num1/den1 + num2/den2.
1426

1427
    Each numerator and denominator is a list of polynomial coefficients.
1428

1429
    """
1430

1431
    num = polyadd(polymul(num1, den2), polymul(num2, den1))
9✔
1432
    den = polymul(den1, den2)
9✔
1433

1434
    return num, den
9✔
1435

1436

1437
def _convert_to_transfer_function(
9✔
1438
        sys, inputs=1, outputs=1, use_prefix_suffix=False):
1439
    """Convert a system to transfer function form (if needed).
1440

1441
    If sys is already a transfer function, then it is returned.  If sys is a
1442
    state space object, then it is converted to a transfer function and
1443
    returned.  If sys is a scalar, then the number of inputs and outputs can be
1444
    specified manually, as in:
1445

1446
    >>> sys = _convert_to_transfer_function(3.) # Assumes inputs = outputs = 1
1447
    >>> sys = _convert_to_transfer_function(1., inputs=3, outputs=2)
1448

1449
    In the latter example, sys's matrix transfer function is [[1., 1., 1.]
1450
                                                              [1., 1., 1.]].
1451

1452
    If sys is an array-like type, then it is converted to a constant-gain
1453
    transfer function.
1454

1455
    Note: no renaming of inputs and outputs is performed; this should be done
1456
    by the calling function.
1457

1458
    >>> sys = _convert_to_transfer_function([[1., 0.], [2., 3.]])
1459

1460
    In this example, the numerator matrix will be
1461
       [[[1.0], [0.0]], [[2.0], [3.0]]]
1462
    and the denominator matrix [[[1.0], [1.0]], [[1.0], [1.0]]]
1463

1464
    """
1465
    from .statesp import StateSpace
9✔
1466
    kwargs = {}
9✔
1467

1468
    if isinstance(sys, TransferFunction):
9✔
1469
        return sys
9✔
1470

1471
    elif isinstance(sys, StateSpace):
9✔
1472
        if 0 == sys.nstates:
9✔
1473
            # Slycot doesn't like static SS->TF conversion, so handle
1474
            # it first.  Can't join this with the no-Slycot branch,
1475
            # since that doesn't handle general MIMO systems
1476
            num = [[[sys.D[i, j]] for j in range(sys.ninputs)]
9✔
1477
                   for i in range(sys.noutputs)]
1478
            den = [[[1.] for j in range(sys.ninputs)]
9✔
1479
                   for i in range(sys.noutputs)]
1480
        else:
1481
            try:
9✔
1482
                # Use Slycot to make the transformation
1483
                # Make sure to convert system matrices to numpy arrays
1484
                from slycot import tb04ad
9✔
1485
                tfout = tb04ad(
5✔
1486
                    sys.nstates, sys.ninputs, sys.noutputs, array(sys.A),
1487
                    array(sys.B), array(sys.C), array(sys.D), tol1=0.0)
1488

1489
                # Preallocate outputs.
1490
                num = [[[] for j in range(sys.ninputs)]
5✔
1491
                       for i in range(sys.noutputs)]
1492
                den = [[[] for j in range(sys.ninputs)]
5✔
1493
                       for i in range(sys.noutputs)]
1494

1495
                for i in range(sys.noutputs):
5✔
1496
                    for j in range(sys.ninputs):
5✔
1497
                        num[i][j] = list(tfout[6][i, j, :])
5✔
1498
                        # Each transfer function matrix row
1499
                        # has a common denominator.
1500
                        den[i][j] = list(tfout[5][i, :])
5✔
1501

1502
            except ImportError:
4✔
1503
                # If slycot is not available, use signal.lti (SISO only)
1504
                if sys.ninputs != 1 or sys.noutputs != 1:
4✔
1505
                    raise ControlMIMONotImplemented("Not implemented for " +
×
1506
                        "MIMO systems without slycot.")
1507

1508
                # Do the conversion using sp.signal.ss2tf
1509
                # Note that this returns a 2D array for the numerator
1510
                num, den = sp.signal.ss2tf(sys.A, sys.B, sys.C, sys.D)
4✔
1511
                num = squeeze(num)  # Convert to 1D array
4✔
1512
                den = squeeze(den)  # Probably not needed
4✔
1513

1514
        newsys = TransferFunction(num, den, sys.dt)
9✔
1515
        if use_prefix_suffix:
9✔
1516
            newsys._copy_names(sys, prefix_suffix_name='converted')
9✔
1517
        return newsys
9✔
1518

1519
    elif isinstance(sys, (int, float, complex, np.number)):
9✔
1520
        num = [[[sys] for j in range(inputs)] for i in range(outputs)]
9✔
1521
        den = [[[1] for j in range(inputs)] for i in range(outputs)]
9✔
1522

1523
        return TransferFunction(num, den)
9✔
1524

1525
    elif isinstance(sys, FrequencyResponseData):
9✔
1526
        raise TypeError("Can't convert given FRD to TransferFunction system.")
9✔
1527

1528
    # If this is array-like, try to create a constant feedthrough
1529
    try:
9✔
1530
        D = array(sys, ndmin=2)
9✔
1531
        outputs, inputs = D.shape
9✔
1532
        num = [[[D[i, j]] for j in range(inputs)] for i in range(outputs)]
9✔
1533
        den = [[[1] for j in range(inputs)] for i in range(outputs)]
9✔
1534
        return TransferFunction(num, den)
9✔
1535

1536
    except Exception:
9✔
1537
        raise TypeError("Can't convert given type to TransferFunction system.")
9✔
1538

1539

1540
def tf(*args, **kwargs):
9✔
1541
    """tf(num, den[, dt])
1542

1543
    Create a transfer function system. Can create MIMO systems.
1544

1545
    The function accepts either 1, 2, or 3 parameters:
1546

1547
    ``tf(sys)``
1548
        Convert a linear system into transfer function form. Always creates
1549
        a new system, even if sys is already a TransferFunction object.
1550

1551
    ``tf(num, den)``
1552
        Create a transfer function system from its numerator and denominator
1553
        polynomial coefficients.
1554

1555
        If `num` and `den` are 1D array_like objects, the function creates a
1556
        SISO system.
1557

1558
        To create a MIMO system, `num` and `den` need to be 2D nested lists
1559
        of array_like objects. (A 3 dimensional data structure in total.)
1560
        (For details see note below.)
1561

1562
    ``tf(num, den, dt)``
1563
        Create a discrete time transfer function system; dt can either be a
1564
        positive number indicating the sampling time or 'True' if no
1565
        specific timebase is given.
1566

1567
    ``tf('s')`` or ``tf('z')``
1568
        Create a transfer function representing the differential operator
1569
        ('s') or delay operator ('z').
1570

1571
    Parameters
1572
    ----------
1573
    sys : LTI (StateSpace or TransferFunction)
1574
        A linear system
1575
    num : array_like, or list of list of array_like
1576
        Polynomial coefficients of the numerator
1577
    den : array_like, or list of list of array_like
1578
        Polynomial coefficients of the denominator
1579
    display_format : None, 'poly' or 'zpk'
1580
        Set the display format used in printing the TransferFunction object.
1581
        Default behavior is polynomial display and can be changed by
1582
        changing config.defaults['xferfcn.display_format']..
1583

1584
    Returns
1585
    -------
1586
    out: :class:`TransferFunction`
1587
        The new linear system
1588

1589
    Other Parameters
1590
    ----------------
1591
    inputs, outputs : str, or list of str, optional
1592
        List of strings that name the individual signals of the transformed
1593
        system.  If not given, the inputs and outputs are the same as the
1594
        original system.
1595
    name : string, optional
1596
        System name. If unspecified, a generic name <sys[id]> is generated
1597
        with a unique integer id.
1598

1599
    Raises
1600
    ------
1601
    ValueError
1602
        if `num` and `den` have invalid or unequal dimensions
1603
    TypeError
1604
        if `num` or `den` are of incorrect type
1605

1606
    See Also
1607
    --------
1608
    TransferFunction
1609
    ss
1610
    ss2tf
1611
    tf2ss
1612

1613
    Notes
1614
    -----
1615
    ``num[i][j]`` contains the polynomial coefficients of the numerator
1616
    for the transfer function from the (j+1)st input to the (i+1)st output.
1617
    ``den[i][j]`` works the same way.
1618

1619
    The list ``[2, 3, 4]`` denotes the polynomial :math:`2s^2 + 3s + 4`.
1620

1621
    The special forms ``tf('s')`` and ``tf('z')`` can be used to create
1622
    transfer functions for differentiation and unit delays.
1623

1624
    Examples
1625
    --------
1626
    >>> # Create a MIMO transfer function object
1627
    >>> # The transfer function from the 2nd input to the 1st output is
1628
    >>> # (3s + 4) / (6s^2 + 5s + 4).
1629
    >>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
1630
    >>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
1631
    >>> sys1 = ct.tf(num, den)
1632

1633
    >>> # Create a variable 's' to allow algebra operations for SISO systems
1634
    >>> s = ct.tf('s')
1635
    >>> G  = (s + 1)/(s**2 + 2*s + 1)
1636

1637
    >>> # Convert a StateSpace to a TransferFunction object.
1638
    >>> sys_ss = ct.ss([[1, -2], [3, -4]], [[5], [7]], [[6, 8]], 9)
1639
    >>> sys_tf = ct.tf(sys_ss)
1640

1641
    """
1642

1643
    if len(args) == 2 or len(args) == 3:
9✔
1644
        return TransferFunction(*args, **kwargs)
9✔
1645

1646
    elif len(args) == 1 and isinstance(args[0], str):
9✔
1647
        # Make sure there were no extraneous keywords
1648
        if kwargs:
9✔
1649
            raise TypeError("unrecognized keywords: ", str(kwargs))
×
1650

1651
        # Look for special cases defining differential/delay operator
1652
        if args[0] == 's':
9✔
1653
            return TransferFunction.s
9✔
1654
        elif args[0] == 'z':
9✔
1655
            return TransferFunction.z
9✔
1656

1657
    elif len(args) == 1:
9✔
1658
        from .statesp import StateSpace
9✔
1659
        sys = args[0]
9✔
1660
        if isinstance(sys, StateSpace):
9✔
1661
            return ss2tf(sys, **kwargs)
9✔
1662
        elif isinstance(sys, TransferFunction):
9✔
1663
            # Use copy constructor
1664
            return TransferFunction(sys, **kwargs)
9✔
1665
        else:
1666
            raise TypeError("tf(sys): sys must be a StateSpace or "
×
1667
                            "TransferFunction object.   It is %s." % type(sys))
1668
    else:
1669
        raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))
×
1670

1671

1672
def zpk(zeros, poles, gain, *args, **kwargs):
9✔
1673
    """zpk(zeros, poles, gain[, dt])
1674

1675
    Create a transfer function from zeros, poles, gain.
1676

1677
    Given a list of zeros z_i, poles p_j, and gain k, return the transfer
1678
    function:
1679

1680
    .. math::
1681
      H(s) = k \\frac{(s - z_1) (s - z_2) \\cdots (s - z_m)}
1682
                     {(s - p_1) (s - p_2) \\cdots (s - p_n)}
1683

1684
    Parameters
1685
    ----------
1686
    zeros : array_like
1687
        Array containing the location of zeros.
1688
    poles : array_like
1689
        Array containing the location of poles.
1690
    gain : float
1691
        System gain
1692
    dt : None, True or float, optional
1693
        System timebase. 0 (default) indicates continuous
1694
        time, True indicates discrete time with unspecified sampling
1695
        time, positive number is discrete time with specified
1696
        sampling time, None indicates unspecified timebase (either
1697
        continuous or discrete time).
1698
    inputs, outputs, states : str, or list of str, optional
1699
        List of strings that name the individual signals.  If this parameter
1700
        is not given or given as `None`, the signal names will be of the
1701
        form `s[i]` (where `s` is one of `u`, `y`, or `x`). See
1702
        :class:`InputOutputSystem` for more information.
1703
    name : string, optional
1704
        System name (used for specifying signals). If unspecified, a generic
1705
        name <sys[id]> is generated with a unique integer id.
1706
    display_format : None, 'poly' or 'zpk', optional
1707
        Set the display format used in printing the TransferFunction object.
1708
        Default behavior is polynomial display and can be changed by
1709
        changing config.defaults['xferfcn.display_format'].
1710

1711
    Returns
1712
    -------
1713
    out: :class:`TransferFunction`
1714
        Transfer function with given zeros, poles, and gain.
1715

1716
    Examples
1717
    --------
1718
    >>> G = ct.zpk([1], [2, 3], gain=1, display_format='zpk')
1719
    >>> print(G)                                                # doctest: +SKIP
1720

1721
         s - 1
1722
    ---------------
1723
    (s - 2) (s - 3)
1724

1725
    """
1726
    num, den = zpk2tf(zeros, poles, gain)
9✔
1727
    return TransferFunction(num, den, *args, **kwargs)
9✔
1728

1729

1730
def ss2tf(*args, **kwargs):
9✔
1731

1732
    """ss2tf(sys)
1733

1734
    Transform a state space system to a transfer function.
1735

1736
    The function accepts either 1 or 4 parameters:
1737

1738
    ``ss2tf(sys)``
1739
        Convert a linear system from state space into transfer function
1740
        form. Always creates a new system.
1741

1742
    ``ss2tf(A, B, C, D)``
1743
        Create a transfer function system from the matrices of its state and
1744
        output equations.
1745

1746
        For details see: :func:`tf`
1747

1748
    Parameters
1749
    ----------
1750
    sys : StateSpace
1751
        A linear system
1752
    A : array_like or string
1753
        System matrix
1754
    B : array_like or string
1755
        Control matrix
1756
    C : array_like or string
1757
        Output matrix
1758
    D : array_like or string
1759
        Feedthrough matrix
1760
    **kwargs : keyword arguments
1761
        Additional arguments passed to :func:`tf` (e.g., signal names)
1762

1763
    Returns
1764
    -------
1765
    out: TransferFunction
1766
        New linear system in transfer function form
1767

1768
    Other Parameters
1769
    ----------------
1770
    inputs, outputs : str, or list of str, optional
1771
        List of strings that name the individual signals of the transformed
1772
        system.  If not given, the inputs and outputs are the same as the
1773
        original system.
1774
    name : string, optional
1775
        System name. If unspecified, a generic name <sys[id]> is generated
1776
        with a unique integer id.
1777

1778
    Raises
1779
    ------
1780
    ValueError
1781
        if matrix sizes are not self-consistent, or if an invalid number of
1782
        arguments is passed in
1783
    TypeError
1784
        if `sys` is not a StateSpace object
1785

1786
    See Also
1787
    --------
1788
    tf
1789
    ss
1790
    tf2ss
1791

1792
    Examples
1793
    --------
1794
    >>> A = [[-1, -2], [3, -4]]
1795
    >>> B = [[5], [6]]
1796
    >>> C = [[7, 8]]
1797
    >>> D = [[9]]
1798
    >>> sys1 = ct.ss2tf(A, B, C, D)
1799

1800
    >>> sys_ss = ct.ss(A, B, C, D)
1801
    >>> sys_tf = ct.ss2tf(sys_ss)
1802

1803
    """
1804

1805
    from .statesp import StateSpace
9✔
1806
    if len(args) == 4 or len(args) == 5:
9✔
1807
        # Assume we were given the A, B, C, D matrix and (optional) dt
1808
        return _convert_to_transfer_function(StateSpace(*args, **kwargs))
9✔
1809

1810
    if len(args) == 1:
9✔
1811
        sys = args[0]
9✔
1812
        if isinstance(sys, StateSpace):
9✔
1813
            kwargs = kwargs.copy()
9✔
1814
            if not kwargs.get('inputs'):
9✔
1815
                kwargs['inputs'] = sys.input_labels
9✔
1816
            if not kwargs.get('outputs'):
9✔
1817
                kwargs['outputs'] = sys.output_labels
9✔
1818
            return TransferFunction(
9✔
1819
                _convert_to_transfer_function(
1820
                    sys, use_prefix_suffix=not sys._generic_name_check()),
1821
                **kwargs)
1822
        else:
1823
            raise TypeError(
×
1824
                "ss2tf(sys): sys must be a StateSpace object.  It is %s."
1825
                % type(sys))
1826
    else:
1827
        raise ValueError("Needs 1 or 4 arguments; received %i." % len(args))
×
1828

1829

1830
def tfdata(sys):
9✔
1831
    """
1832
    Return transfer function data objects for a system.
1833

1834
    Parameters
1835
    ----------
1836
    sys : LTI (StateSpace, or TransferFunction)
1837
        LTI system whose data will be returned
1838

1839
    Returns
1840
    -------
1841
    (num, den): numerator and denominator arrays
1842
        Transfer function coefficients (SISO only)
1843
    """
1844
    tf = _convert_to_transfer_function(sys)
9✔
1845

1846
    return tf.num, tf.den
9✔
1847

1848

1849
def _clean_part(data):
9✔
1850
    """
1851
    Return a valid, cleaned up numerator or denominator
1852
    for the TransferFunction class.
1853

1854
    Parameters
1855
    ----------
1856
    data : numerator or denominator of a transfer function.
1857

1858
    Returns
1859
    -------
1860
    data: list of lists of ndarrays, with int converted to float
1861
    """
1862
    valid_types = (int, float, complex, np.number)
9✔
1863
    valid_collection = (list, tuple, ndarray)
9✔
1864

1865
    if (isinstance(data, valid_types) or
9✔
1866
            (isinstance(data, ndarray) and data.ndim == 0)):
1867
        # Data is a scalar (including 0d ndarray)
1868
        data = [[array([data])]]
9✔
1869
    elif (isinstance(data, ndarray) and data.ndim == 3 and
9✔
1870
          isinstance(data[0, 0, 0], valid_types)):
1871
        data = [[array(data[i, j])
9✔
1872
                 for j in range(data.shape[1])]
1873
                for i in range(data.shape[0])]
1874
    elif (isinstance(data, valid_collection) and
9✔
1875
            all([isinstance(d, valid_types) for d in data])):
1876
        data = [[array(data)]]
9✔
1877
    elif (isinstance(data, (list, tuple)) and
9✔
1878
          isinstance(data[0], (list, tuple)) and
1879
          (isinstance(data[0][0], valid_collection) and
1880
           all([isinstance(d, valid_types) for d in data[0][0]]))):
1881
        data = list(data)
9✔
1882
        for j in range(len(data)):
9✔
1883
            data[j] = list(data[j])
9✔
1884
            for k in range(len(data[j])):
9✔
1885
                data[j][k] = array(data[j][k])
9✔
1886
    else:
1887
        # If the user passed in anything else, then it's unclear what
1888
        # the meaning is.
1889
        raise TypeError(
9✔
1890
            "The numerator and denominator inputs must be scalars or vectors "
1891
            "(for\nSISO), or lists of lists of vectors (for SISO or MIMO).")
1892

1893
    # Check for coefficients that are ints and convert to floats
1894
    for i in range(len(data)):
9✔
1895
        for j in range(len(data[i])):
9✔
1896
            for k in range(len(data[i][j])):
9✔
1897
                if isinstance(data[i][j][k], (int, np.int32, np.int64)):
9✔
1898
                    data[i][j][k] = float(data[i][j][k])
9✔
1899

1900
    return data
9✔
1901

1902

1903
# Define constants to represent differentiation, unit delay
1904
TransferFunction.s = TransferFunction([1, 0], [1], 0, name='s')
9✔
1905
TransferFunction.z = TransferFunction([1, 0], [1], True, name='z')
9✔
1906

1907

1908
def _float2str(value):
9✔
1909
    _num_format = config.defaults.get('xferfcn.floating_point_format', ':.4g')
9✔
1910
    return f"{value:{_num_format}}"
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