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

python-control / python-control / 12862231340

20 Jan 2025 06:23AM UTC coverage: 94.648% (-0.04%) from 94.69%
12862231340

push

github

web-flow
Merge pull request #1081 from sdahdah/main

Support for binary operations between MIMO and SISO LTI systems

9673 of 10220 relevant lines covered (94.65%)

8.26 hits per line

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

99.28
control/bdalg.py
1
"""bdalg.py
2

3
This file contains some standard block diagram algebra.
4

5
Routines in this module:
6

7
append
8
series
9
parallel
10
negate
11
feedback
12
connect
13
combine_tf
14
split_tf
15

16
"""
17

18
"""Copyright (c) 2010 by California Institute of Technology
9✔
19
All rights reserved.
20

21
Redistribution and use in source and binary forms, with or without
22
modification, are permitted provided that the following conditions
23
are met:
24

25
1. Redistributions of source code must retain the above copyright
26
   notice, this list of conditions and the following disclaimer.
27

28
2. Redistributions in binary form must reproduce the above copyright
29
   notice, this list of conditions and the following disclaimer in the
30
   documentation and/or other materials provided with the distribution.
31

32
3. Neither the name of the California Institute of Technology nor
33
   the names of its contributors may be used to endorse or promote
34
   products derived from this software without specific prior
35
   written permission.
36

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

50
Author: Richard M. Murray
51
Date: 24 May 09
52
Revised: Kevin K. Chen, Dec 10
53

54
$Id$
55

56
"""
57

58
from functools import reduce
9✔
59
from warnings import warn
9✔
60

61
import numpy as np
9✔
62

63
from . import frdata as frd
9✔
64
from . import statesp as ss
9✔
65
from . import xferfcn as tf
9✔
66
from .iosys import InputOutputSystem
9✔
67

68
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect',
9✔
69
           'combine_tf', 'split_tf']
70

71

72
def series(sys1, *sysn, **kwargs):
9✔
73
    r"""series(sys1, sys2, [..., sysn])
74

75
    Return the series connection (`sysn` \* ...\  \*) `sys2` \* `sys1`.
76

77
    Parameters
78
    ----------
79
    sys1, sys2, ..., sysn : scalar, array, or :class:`InputOutputSystem`
80
        I/O systems to combine.
81

82
    Returns
83
    -------
84
    out : scalar, array, or :class:`InputOutputSystem`
85
        Series interconnection of the systems.
86

87
    Other Parameters
88
    ----------------
89
    inputs, outputs : str, or list of str, optional
90
        List of strings that name the individual signals.  If not given,
91
        signal names will be of the form `s[i]` (where `s` is one of `u`,
92
        or `y`). See :class:`InputOutputSystem` for more information.
93
    states : str, or list of str, optional
94
        List of names for system states.  If not given, state names will be
95
        of of the form `x[i]` for interconnections of linear systems or
96
        '<subsys_name>.<state_name>' for interconnected nonlinear systems.
97
    name : string, optional
98
        System name (used for specifying signals). If unspecified, a generic
99
        name <sys[id]> is generated with a unique integer id.
100

101
    Raises
102
    ------
103
    ValueError
104
        if `sys2.ninputs` does not equal `sys1.noutputs`
105
        if `sys1.dt` is not compatible with `sys2.dt`
106

107
    See Also
108
    --------
109
    append, feedback, interconnect, negate, parallel
110

111
    Notes
112
    -----
113
    This function is a wrapper for the __mul__ function in the appropriate
114
    :class:`NonlinearIOSystem`, :class:`StateSpace`,
115
    :class:`TransferFunction`, or other I/O system class.  The output type
116
    is the type of `sys1` unless a more general type is required based on
117
    type type of `sys2`.
118

119
    If both systems have a defined timebase (dt = 0 for continuous time,
120
    dt > 0 for discrete time), then the timebase for both systems must
121
    match.  If only one of the system has a timebase, the return
122
    timebase will be set to match it.
123

124
    Examples
125
    --------
126
    >>> G1 = ct.rss(3)
127
    >>> G2 = ct.rss(4)
128
    >>> G = ct.series(G1, G2) # Same as sys3 = sys2 * sys1
129
    >>> G.ninputs, G.noutputs, G.nstates
130
    (1, 1, 7)
131

132
    >>> G1 = ct.rss(2, inputs=2, outputs=3)
133
    >>> G2 = ct.rss(3, inputs=3, outputs=1)
134
    >>> G = ct.series(G1, G2) # Same as sys3 = sys2 * sys1
135
    >>> G.ninputs, G.noutputs, G.nstates
136
    (2, 1, 5)
137

138
    """
139
    sys = reduce(lambda x, y: y * x, sysn, sys1)
9✔
140
    sys.update_names(**kwargs)
9✔
141
    return sys
9✔
142

143

144
def parallel(sys1, *sysn, **kwargs):
9✔
145
    r"""parallel(sys1, sys2, [..., sysn])
146

147
    Return the parallel connection `sys1` + `sys2` (+ ...\  + `sysn`).
148

149
    Parameters
150
    ----------
151
    sys1, sys2, ..., sysn : scalar, array, or :class:`InputOutputSystem`
152
        I/O systems to combine.
153

154
    Returns
155
    -------
156
    out : scalar, array, or :class:`InputOutputSystem`
157
        Parallel interconnection of the systems.
158

159
    Other Parameters
160
    ----------------
161
    inputs, outputs : str, or list of str, optional
162
        List of strings that name the individual signals.  If not given,
163
        signal names will be of the form `s[i]` (where `s` is one of `u`,
164
        or `y`). See :class:`InputOutputSystem` for more information.
165
    states : str, or list of str, optional
166
        List of names for system states.  If not given, state names will be
167
        of the form `x[i]` for interconnections of linear systems or
168
        '<subsys_name>.<state_name>' for interconnected nonlinear systems.
169
    name : string, optional
170
        System name (used for specifying signals). If unspecified, a generic
171
        name <sys[id]> is generated with a unique integer id.
172

173
    Raises
174
    ------
175
    ValueError
176
        if `sys1` and `sys2` do not have the same numbers of inputs and outputs
177

178
    See Also
179
    --------
180
    append, feedback, interconnect, negate, series
181

182
    Notes
183
    -----
184
    This function is a wrapper for the __add__ function in the
185
    StateSpace and TransferFunction classes.  The output type is usually
186
    the type of `sys1`.  If `sys1` is a scalar, then the output type is
187
    the type of `sys2`.
188

189
    If both systems have a defined timebase (dt = 0 for continuous time,
190
    dt > 0 for discrete time), then the timebase for both systems must
191
    match.  If only one of the system has a timebase, the return
192
    timebase will be set to match it.
193

194
    Examples
195
    --------
196
    >>> G1 = ct.rss(3)
197
    >>> G2 = ct.rss(4)
198
    >>> G = ct.parallel(G1, G2) # Same as sys3 = sys1 + sys2
199
    >>> G.ninputs, G.noutputs, G.nstates
200
    (1, 1, 7)
201

202
    >>> G1 = ct.rss(3, inputs=3, outputs=4)
203
    >>> G2 = ct.rss(4, inputs=3, outputs=4)
204
    >>> G = ct.parallel(G1, G2)  # Add another system
205
    >>> G.ninputs, G.noutputs, G.nstates
206
    (3, 4, 7)
207

208
    """
209
    sys = reduce(lambda x, y: x + y, sysn, sys1)
9✔
210
    sys.update_names(**kwargs)
9✔
211
    return sys
9✔
212

213
def negate(sys, **kwargs):
9✔
214
    """
215
    Return the negative of a system.
216

217
    Parameters
218
    ----------
219
    sys : scalar, array, or :class:`InputOutputSystem`
220
        I/O systems to negate.
221

222
    Returns
223
    -------
224
    out : scalar, array, or :class:`InputOutputSystem`
225
        Negated system.
226

227
    Other Parameters
228
    ----------------
229
    inputs, outputs : str, or list of str, optional
230
        List of strings that name the individual signals.  If not given,
231
        signal names will be of the form `s[i]` (where `s` is one of `u`,
232
        or `y`). See :class:`InputOutputSystem` for more information.
233
    states : str, or list of str, optional
234
        List of names for system states.  If not given, state names will be
235
        of of the form `x[i]` for interconnections of linear systems or
236
        '<subsys_name>.<state_name>' for interconnected nonlinear systems.
237
    name : string, optional
238
        System name (used for specifying signals). If unspecified, a generic
239
        name <sys[id]> is generated with a unique integer id.
240

241
    See Also
242
    --------
243
    append, feedback, interconnect, parallel, series
244

245
    Notes
246
    -----
247
    This function is a wrapper for the __neg__ function in the StateSpace and
248
    TransferFunction classes.  The output type is the same as the input type.
249

250
    Examples
251
    --------
252
    >>> G = ct.tf([2], [1, 1])
253
    >>> G.dcgain()
254
    np.float64(2.0)
255

256
    >>> Gn = ct.negate(G) # Same as sys2 = -sys1.
257
    >>> Gn.dcgain()
258
    np.float64(-2.0)
259

260
    """
261
    sys = -sys
9✔
262
    sys.update_names(**kwargs)
9✔
263
    return sys
9✔
264

265
#! TODO: expand to allow sys2 default to work in MIMO case?
266
def feedback(sys1, sys2=1, sign=-1, **kwargs):
9✔
267
    """Feedback interconnection between two I/O systems.
268

269
    Parameters
270
    ----------
271
    sys1, sys2 : scalar, array, or :class:`InputOutputSystem`
272
        I/O systems to combine.
273
    sign : scalar
274
        The sign of feedback.  `sign` = -1 indicates negative feedback, and
275
        `sign` = 1 indicates positive feedback.  `sign` is an optional
276
        argument; it assumes a value of -1 if not specified.
277

278
    Returns
279
    -------
280
    out : scalar, array, or :class:`InputOutputSystem`
281
        Feedback interconnection of the systems.
282

283
    Other Parameters
284
    ----------------
285
    inputs, outputs : str, or list of str, optional
286
        List of strings that name the individual signals.  If not given,
287
        signal names will be of the form `s[i]` (where `s` is one of `u`,
288
        or `y`). See :class:`InputOutputSystem` for more information.
289
    states : str, or list of str, optional
290
        List of names for system states.  If not given, state names will be
291
        of of the form `x[i]` for interconnections of linear systems or
292
        '<subsys_name>.<state_name>' for interconnected nonlinear systems.
293
    name : string, optional
294
        System name (used for specifying signals). If unspecified, a generic
295
        name <sys[id]> is generated with a unique integer id.
296

297
    Raises
298
    ------
299
    ValueError
300
        if `sys1` does not have as many inputs as `sys2` has outputs, or if
301
        `sys2` does not have as many inputs as `sys1` has outputs
302
    NotImplementedError
303
        if an attempt is made to perform a feedback on a MIMO TransferFunction
304
        object
305

306
    See Also
307
    --------
308
    append, interconnect, negate, parallel, series
309

310
    Notes
311
    -----
312
    This function is a wrapper for the `feedback` function in the I/O
313
    system classes.  It calls sys1.feedback if `sys1` is an I/O system
314
    object.  If `sys1` is a scalar, then it is converted to `sys2`'s type,
315
    and the corresponding feedback function is used.
316

317
    Examples
318
    --------
319
    >>> G = ct.rss(3, inputs=2, outputs=5)
320
    >>> C = ct.rss(4, inputs=5, outputs=2)
321
    >>> T = ct.feedback(G, C, sign=1)
322
    >>> T.ninputs, T.noutputs, T.nstates
323
    (2, 5, 7)
324

325
    """
326
    # Allow anything with a feedback function to call that function
327
    # TODO: rewrite to allow __rfeedback__
328
    try:
9✔
329
        return sys1.feedback(sys2, sign, **kwargs)
9✔
330
    except (AttributeError, TypeError):
9✔
331
        pass
9✔
332

333
    # Check for correct input types
334
    if not isinstance(sys1, (int, float, complex, np.number, np.ndarray,
9✔
335
                             InputOutputSystem)):
336
        raise TypeError("sys1 must be an I/O system, scalar, or array")
9✔
337
    elif not isinstance(sys2, (int, float, complex, np.number, np.ndarray,
9✔
338
                               InputOutputSystem)):
339
        raise TypeError("sys2 must be an I/O system, scalar, or array")
9✔
340

341
    # If sys1 is a scalar or ndarray, use the type of sys2 to figure
342
    # out how to convert sys1, using transfer functions whenever possible.
343
    if isinstance(sys1, (int, float, complex, np.number, np.ndarray)):
9✔
344
        if isinstance(sys2, (int, float, complex, np.number, np.ndarray,
9✔
345
                             tf.TransferFunction)):
346
            sys1 = tf._convert_to_transfer_function(sys1)
9✔
347
        elif isinstance(sys2, frd.FrequencyResponseData):
9✔
348
            sys1 = frd._convert_to_frd(sys1, sys2.omega)
9✔
349
        else:
350
            sys1 = ss._convert_to_statespace(sys1)
9✔
351

352
    sys = sys1.feedback(sys2, sign)
9✔
353
    sys.update_names(**kwargs)
9✔
354
    return sys
9✔
355

356
def append(*sys, **kwargs):
9✔
357
    """append(sys1, sys2, [..., sysn])
358

359
    Group LTI models by appending their inputs and outputs.
360

361
    Forms an augmented system model, and appends the inputs and
362
    outputs together.
363

364
    Parameters
365
    ----------
366
    sys1, sys2, ..., sysn : scalar, array, or :class:`LTI`
367
        I/O systems to combine.
368

369
    Other Parameters
370
    ----------------
371
    inputs, outputs : str, or list of str, optional
372
        List of strings that name the individual signals.  If not given,
373
        signal names will be of the form `s[i]` (where `s` is one of `u`,
374
        or `y`). See :class:`InputOutputSystem` for more information.
375
    states : str, or list of str, optional
376
        List of names for system states.  If not given, state names will be
377
        of of the form `x[i]` for interconnections of linear systems or
378
        '<subsys_name>.<state_name>' for interconnected nonlinear systems.
379
    name : string, optional
380
        System name (used for specifying signals). If unspecified, a generic
381
        name <sys[id]> is generated with a unique integer id.
382

383
    Returns
384
    -------
385
    out : :class:`LTI`
386
        Combined system, with input/output vectors consisting of all
387
        input/output vectors appended. Specific type returned is the type of
388
        the first argument.
389

390
    See Also
391
    --------
392
    interconnect, feedback, negate, parallel, series
393

394
    Examples
395
    --------
396
    >>> G1 = ct.rss(3)
397
    >>> G2 = ct.rss(4)
398
    >>> G = ct.append(G1, G2)
399
    >>> G.ninputs, G.noutputs, G.nstates
400
    (2, 2, 7)
401

402
    >>> G1 = ct.rss(3, inputs=2, outputs=4)
403
    >>> G2 = ct.rss(4, inputs=1, outputs=4)
404
    >>> G = ct.append(G1, G2)
405
    >>> G.ninputs, G.noutputs, G.nstates
406
    (3, 8, 7)
407

408
    """
409
    s1 = sys[0]
9✔
410
    for s in sys[1:]:
9✔
411
        s1 = s1.append(s)
9✔
412
    s1.update_names(**kwargs)
9✔
413
    return s1
9✔
414

415
def connect(sys, Q, inputv, outputv):
9✔
416
    """Index-based interconnection of an LTI system.
417

418
    .. deprecated:: 0.10.0
419
        `connect` will be removed in a future version of python-control.
420
        Use :func:`interconnect` instead, which works with named signals.
421

422
    The system `sys` is a system typically constructed with `append`, with
423
    multiple inputs and outputs.  The inputs and outputs are connected
424
    according to the interconnection matrix `Q`, and then the final inputs and
425
    outputs are trimmed according to the inputs and outputs listed in `inputv`
426
    and `outputv`.
427

428
    NOTE: Inputs and outputs are indexed starting at 1 and negative values
429
    correspond to a negative feedback interconnection.
430

431
    Parameters
432
    ----------
433
    sys : :class:`InputOutputSystem`
434
        System to be connected.
435
    Q : 2D array
436
        Interconnection matrix. First column gives the input to be connected.
437
        The second column gives the index of an output that is to be fed into
438
        that input. Each additional column gives the index of an additional
439
        input that may be optionally added to that input. Negative
440
        values mean the feedback is negative. A zero value is ignored. Inputs
441
        and outputs are indexed starting at 1 to communicate sign information.
442
    inputv : 1D array
443
        list of final external inputs, indexed starting at 1
444
    outputv : 1D array
445
        list of final external outputs, indexed starting at 1
446

447
    Returns
448
    -------
449
    out : :class:`InputOutputSystem`
450
        Connected and trimmed I/O system.
451

452
    See Also
453
    --------
454
    append, feedback, interconnect, negate, parallel, series
455

456
    Notes
457
    -----
458
    The :func:`~control.interconnect` function in the :ref:`input/output
459
    systems <iosys-module>` module allows the use of named signals and
460
    provides an alternative method for interconnecting multiple systems.
461

462
    Examples
463
    --------
464
    >>> G = ct.rss(7, inputs=2, outputs=2)
465
    >>> K = [[1, 2], [2, -1]]  # negative feedback interconnection
466
    >>> T = ct.connect(G, K, [2], [1, 2])
467
    >>> T.ninputs, T.noutputs, T.nstates
468
    (1, 2, 7)
469

470
    """
471
    # TODO: maintain `connect` for use in MATLAB submodule (?)
472
    warn("connect() is deprecated; use interconnect()", FutureWarning)
9✔
473

474
    inputv, outputv, Q = \
9✔
475
        np.atleast_1d(inputv), np.atleast_1d(outputv), np.atleast_1d(Q)
476
    # check indices
477
    index_errors = (inputv - 1 > sys.ninputs) | (inputv < 1)
9✔
478
    if np.any(index_errors):
9✔
479
        raise IndexError(
9✔
480
            "inputv index %s out of bounds" % inputv[np.where(index_errors)])
481
    index_errors = (outputv - 1 > sys.noutputs) | (outputv < 1)
9✔
482
    if np.any(index_errors):
9✔
483
        raise IndexError(
9✔
484
            "outputv index %s out of bounds" % outputv[np.where(index_errors)])
485
    index_errors = (Q[:,0:1] - 1 > sys.ninputs) | (Q[:,0:1] < 1)
9✔
486
    if np.any(index_errors):
9✔
487
        raise IndexError(
9✔
488
            "Q input index %s out of bounds" % Q[np.where(index_errors)])
489
    index_errors = (np.abs(Q[:,1:]) - 1 > sys.noutputs)
9✔
490
    if np.any(index_errors):
9✔
491
        raise IndexError(
9✔
492
            "Q output index %s out of bounds" % Q[np.where(index_errors)])
493

494
    # first connect
495
    K = np.zeros((sys.ninputs, sys.noutputs))
9✔
496
    for r in np.array(Q).astype(int):
9✔
497
        inp = r[0]-1
9✔
498
        for outp in r[1:]:
9✔
499
            if outp < 0:
9✔
500
                K[inp,-outp-1] = -1.
9✔
501
            elif outp > 0:
9✔
502
                K[inp,outp-1] = 1.
9✔
503
    sys = sys.feedback(np.array(K), sign=1)
9✔
504

505
    # now trim
506
    Ytrim = np.zeros((len(outputv), sys.noutputs))
9✔
507
    Utrim = np.zeros((sys.ninputs, len(inputv)))
9✔
508
    for i,u in enumerate(inputv):
9✔
509
        Utrim[u-1,i] = 1.
9✔
510
    for i,y in enumerate(outputv):
9✔
511
        Ytrim[i,y-1] = 1.
9✔
512

513
    return Ytrim * sys * Utrim
9✔
514

515
def combine_tf(tf_array, **kwargs):
9✔
516
    """Combine array-like of transfer functions into MIMO transfer function.
517

518
    Parameters
519
    ----------
520
    tf_array : list of list of TransferFunction or array_like
521
        Transfer matrix represented as a two-dimensional array or list-of-lists
522
        containing TransferFunction objects. The TransferFunction objects can
523
        have multiple outputs and inputs, as long as the dimensions are
524
        compatible.
525

526
    Returns
527
    -------
528
    TransferFunction
529
        Transfer matrix represented as a single MIMO TransferFunction object.
530

531
    Other Parameters
532
    ----------------
533
    inputs, outputs : str, or list of str, optional
534
        List of strings that name the individual signals.  If not given,
535
        signal names will be of the form `s[i]` (where `s` is one of `u`,
536
        or `y`). See :class:`InputOutputSystem` for more information.
537
    name : string, optional
538
        System name (used for specifying signals). If unspecified, a generic
539
        name <sys[id]> is generated with a unique integer id.
540

541
    Raises
542
    ------
543
    ValueError
544
        If timesteps of transfer functions do not match.
545
    ValueError
546
        If ``tf_array`` has incorrect dimensions.
547
    ValueError
548
        If the transfer functions in a row have mismatched output or input
549
        dimensions.
550

551
    Examples
552
    --------
553
    Combine two transfer functions
554

555
    >>> s = ct.tf('s')
556
    >>> ct.combine_tf(
557
    ...     [[1 / (s + 1)],
558
    ...      [s / (s + 2)]],
559
    ...     name='G'
560
    ... )
561
    TransferFunction(
562
    [[array([1])],
563
     [array([1, 0])]],
564
    [[array([1, 1])],
565
     [array([1, 2])]],
566
    name='G', outputs=2, inputs=1)
567

568
    Combine NumPy arrays with transfer functions
569

570
    >>> ct.combine_tf(
571
    ...     [[np.eye(2), np.zeros((2, 1))],
572
    ...      [np.zeros((1, 2)), ct.tf([1], [1, 0])]],
573
    ...     name='G'
574
    ... )
575
    TransferFunction(
576
    [[array([1.]), array([0.]), array([0.])],
577
     [array([0.]), array([1.]), array([0.])],
578
     [array([0.]), array([0.]), array([1])]],
579
    [[array([1.]), array([1.]), array([1.])],
580
     [array([1.]), array([1.]), array([1.])],
581
     [array([1.]), array([1.]), array([1, 0])]],
582
    name='G', outputs=3, inputs=3)
583
    """
584
    # Find common timebase or raise error
585
    dt_list = []
9✔
586
    try:
9✔
587
        for row in tf_array:
9✔
588
            for tfn in row:
9✔
589
                dt_list.append(getattr(tfn, "dt", None))
9✔
590
    except OSError:
9✔
591
        raise ValueError("`tf_array` has too few dimensions.")
9✔
592
    dt_set = set(dt_list)
9✔
593
    dt_set.discard(None)
9✔
594
    if len(dt_set) > 1:
9✔
595
        raise ValueError("Timesteps of transfer functions are "
9✔
596
                         f"mismatched: {dt_set}")
597
    elif len(dt_set) == 0:
9✔
598
        dt = None
9✔
599
    else:
600
        dt = dt_set.pop()
9✔
601
    # Convert all entries to transfer function objects
602
    ensured_tf_array = []
9✔
603
    for row in tf_array:
9✔
604
        ensured_row = []
9✔
605
        for tfn in row:
9✔
606
            ensured_row.append(_ensure_tf(tfn, dt))
9✔
607
        ensured_tf_array.append(ensured_row)
9✔
608
    # Iterate over
609
    num = []
9✔
610
    den = []
9✔
611
    for row_index, row in enumerate(ensured_tf_array):
9✔
612
        for j_out in range(row[0].noutputs):
9✔
613
            num_row = []
9✔
614
            den_row = []
9✔
615
            for col in row:
9✔
616
                if col.noutputs != row[0].noutputs:
9✔
617
                    raise ValueError(
9✔
618
                        "Mismatched number of transfer function outputs in "
619
                        f"row {row_index}."
620
                    )
621
                for j_in in range(col.ninputs):
9✔
622
                    num_row.append(col.num_array[j_out, j_in])
9✔
623
                    den_row.append(col.den_array[j_out, j_in])
9✔
624
            num.append(num_row)
9✔
625
            den.append(den_row)
9✔
626
    for row_index, row in enumerate(num):
9✔
627
        if len(row) != len(num[0]):
9✔
628
            raise ValueError(
9✔
629
                "Mismatched number transfer function inputs in row "
630
                f"{row_index} of numerator."
631
            )
632
    for row_index, row in enumerate(den):
9✔
633
        if len(row) != len(den[0]):
9✔
634
            raise ValueError(
×
635
                "Mismatched number transfer function inputs in row "
636
                f"{row_index} of denominator."
637
            )
638
    return tf.TransferFunction(num, den, dt=dt, **kwargs)
9✔
639

640

641
def split_tf(transfer_function):
9✔
642
    """Split MIMO transfer function into NumPy array of SISO transfer functions.
643

644
    System and signal names for the array of SISO transfer functions are
645
    copied from the MIMO system.
646

647
    Parameters
648
    ----------
649
    transfer_function : TransferFunction
650
        MIMO transfer function to split.
651

652
    Returns
653
    -------
654
    np.ndarray
655
        NumPy array of SISO transfer functions.
656

657
    Examples
658
    --------
659
    Split a MIMO transfer function
660

661
    >>> G = ct.tf(
662
    ...     [ [[87.8], [-86.4]],
663
    ...       [[108.2], [-109.6]] ],
664
    ...     [ [[1, 1], [1, 1]],
665
    ...       [[1, 1], [1, 1]],   ],
666
    ...     name='G'
667
    ... )
668
    >>> ct.split_tf(G)
669
    array([[TransferFunction(
670
            array([87.8]),
671
            array([1, 1]),
672
            name='G', outputs=1, inputs=1), TransferFunction(
673
                                            array([-86.4]),
674
                                            array([1, 1]),
675
                                            name='G', outputs=1, inputs=1)],
676
           [TransferFunction(
677
            array([108.2]),
678
            array([1, 1]),
679
            name='G', outputs=1, inputs=1), TransferFunction(
680
                                            array([-109.6]),
681
                                            array([1, 1]),
682
                                            name='G', outputs=1, inputs=1)]],
683
          dtype=object)
684
    """
685
    tf_split_lst = []
9✔
686
    for i_out in range(transfer_function.noutputs):
9✔
687
        row = []
9✔
688
        for i_in in range(transfer_function.ninputs):
9✔
689
            row.append(
9✔
690
                tf.TransferFunction(
691
                    transfer_function.num_array[i_out, i_in],
692
                    transfer_function.den_array[i_out, i_in],
693
                    dt=transfer_function.dt,
694
                    inputs=transfer_function.input_labels[i_in],
695
                    outputs=transfer_function.output_labels[i_out],
696
                    name=transfer_function.name
697
                )
698
            )
699
        tf_split_lst.append(row)
9✔
700
    return np.array(tf_split_lst, dtype=object)
9✔
701

702
def _ensure_tf(arraylike_or_tf, dt=None):
9✔
703
    """Convert an array-like to a transfer function.
704

705
    Parameters
706
    ----------
707
    arraylike_or_tf : TransferFunction or array_like
708
        Array-like or transfer function.
709
    dt : None, True or float, optional
710
        System timebase. 0 (default) indicates continuous
711
        time, True indicates discrete time with unspecified sampling
712
        time, positive number is discrete time with specified
713
        sampling time, None indicates unspecified timebase (either
714
        continuous or discrete time). If None, timestep is not validated.
715

716
    Returns
717
    -------
718
    TransferFunction
719
        Transfer function.
720

721
    Raises
722
    ------
723
    ValueError
724
        If input cannot be converted to a transfer function.
725
    ValueError
726
        If the timesteps do not match.
727
    """
728
    # If the input is already a transfer function, return it right away
729
    if isinstance(arraylike_or_tf, tf.TransferFunction):
9✔
730
        # If timesteps don't match, raise an exception
731
        if (dt is not None) and (arraylike_or_tf.dt != dt):
9✔
732
            raise ValueError(
9✔
733
                f"`arraylike_or_tf.dt={arraylike_or_tf.dt}` does not match "
734
                f"argument `dt={dt}`."
735
            )
736
        return arraylike_or_tf
9✔
737
    if np.ndim(arraylike_or_tf) > 2:
9✔
738
        raise ValueError(
9✔
739
            "Array-like must have less than two dimensions to be converted "
740
            "into a transfer function."
741
        )
742
    # If it's not, then convert it to a transfer function
743
    arraylike_3d = np.atleast_3d(arraylike_or_tf)
9✔
744
    try:
9✔
745
        tfn = tf.TransferFunction(
9✔
746
            arraylike_3d,
747
            np.ones_like(arraylike_3d),
748
            dt,
749
        )
750
    except TypeError:
9✔
751
        raise ValueError(
9✔
752
            "`arraylike_or_tf` must only contain array-likes or transfer "
753
            "functions."
754
        )
755
    return tfn
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

© 2026 Coveralls, Inc