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

python-control / python-control / 12266904606

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

Pull #1078

github

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

9330 of 9849 relevant lines covered (94.73%)

8.27 hits per line

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

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 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 state space 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:`StateSpace`
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:`StateSpace`
386
        Combined system, with input/output vectors consisting of all
387
        input/output vectors appended.
388

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

512
    return Ytrim * sys * Utrim
9✔
513

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

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

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

530
    Raises
531
    ------
532
    ValueError
533
        If timesteps of transfer functions do not match.
534
    ValueError
535
        If ``tf_array`` has incorrect dimensions.
536
    ValueError
537
        If the transfer functions in a row have mismatched output or input
538
        dimensions.
539

540
    Examples
541
    --------
542
    Combine two transfer functions
543

544
    >>> s = control.TransferFunction.s
545
    >>> control.combine_tf([
546
    ...     [1 / (s + 1)],
547
    ...     [s / (s + 2)],
548
    ... ])
549
    TransferFunction([[array([1])], [array([1, 0])]],
550
                     [[array([1, 1])], [array([1, 2])]])
551

552
    Combine NumPy arrays with transfer functions
553

554
    >>> control.combine_tf([
555
    ...     [np.eye(2), np.zeros((2, 1))],
556
    ...     [np.zeros((1, 2)), control.TransferFunction([1], [1, 0])],
557
    ... ])
558
    TransferFunction([[array([1.]), array([0.]), array([0.])],
559
                      [array([0.]), array([1.]), array([0.])],
560
                      [array([0.]), array([0.]), array([1])]],
561
                     [[array([1.]), array([1.]), array([1.])],
562
                      [array([1.]), array([1.]), array([1.])],
563
                      [array([1.]), array([1.]), array([1, 0])]])
564
    """
565
    # Find common timebase or raise error
566
    dt_list = []
9✔
567
    try:
9✔
568
        for row in tf_array:
9✔
569
            for tfn in row:
9✔
570
                dt_list.append(getattr(tfn, "dt", None))
9✔
571
    except OSError:
9✔
572
        raise ValueError("`tf_array` has too few dimensions.")
9✔
573
    dt_set = set(dt_list)
9✔
574
    dt_set.discard(None)
9✔
575
    if len(dt_set) > 1:
9✔
576
        raise ValueError("Timesteps of transfer functions are "
9✔
577
                         f"mismatched: {dt_set}")
578
    elif len(dt_set) == 0:
9✔
579
        dt = None
9✔
580
    else:
581
        dt = dt_set.pop()
9✔
582
    # Convert all entries to transfer function objects
583
    ensured_tf_array = []
9✔
584
    for row in tf_array:
9✔
585
        ensured_row = []
9✔
586
        for tfn in row:
9✔
587
            ensured_row.append(_ensure_tf(tfn, dt))
9✔
588
        ensured_tf_array.append(ensured_row)
9✔
589
    # Iterate over
590
    num = []
9✔
591
    den = []
9✔
592
    for row_index, row in enumerate(ensured_tf_array):
9✔
593
        for j_out in range(row[0].noutputs):
9✔
594
            num_row = []
9✔
595
            den_row = []
9✔
596
            for col in row:
9✔
597
                if col.noutputs != row[0].noutputs:
9✔
598
                    raise ValueError(
9✔
599
                        "Mismatched number of transfer function outputs in "
600
                        f"row {row_index}."
601
                    )
602
                for j_in in range(col.ninputs):
9✔
603
                    num_row.append(col.num[j_out][j_in])
9✔
604
                    den_row.append(col.den[j_out][j_in])
9✔
605
            num.append(num_row)
9✔
606
            den.append(den_row)
9✔
607
    for row_index, row in enumerate(num):
9✔
608
        if len(row) != len(num[0]):
9✔
609
            raise ValueError(
9✔
610
                "Mismatched number transfer function inputs in row "
611
                f"{row_index} of numerator."
612
            )
613
    for row_index, row in enumerate(den):
9✔
614
        if len(row) != len(den[0]):
9✔
615
            raise ValueError(
×
616
                "Mismatched number transfer function inputs in row "
617
                f"{row_index} of denominator."
618
            )
619
    return tf.TransferFunction(num, den, dt=dt)
9✔
620

621
def split_tf(transfer_function):
9✔
622
    """Split MIMO transfer function into NumPy array of SISO tranfer functions.
623

624
    Parameters
625
    ----------
626
    transfer_function : TransferFunction
627
        MIMO transfer function to split.
628

629
    Returns
630
    -------
631
    np.ndarray
632
        NumPy array of SISO transfer functions.
633

634
    Examples
635
    --------
636
    Split a MIMO transfer function
637

638
    >>> G = control.TransferFunction(
639
    ...     [
640
    ...         [[87.8], [-86.4]],
641
    ...         [[108.2], [-109.6]],
642
    ...     ],
643
    ...     [
644
    ...         [[1, 1], [1, 1]],
645
    ...         [[1, 1], [1, 1]],
646
    ...     ],
647
    ... )
648
    >>> control.split_tf(G)
649
    array([[TransferFunction(array([87.8]), array([1, 1])),
650
            TransferFunction(array([-86.4]), array([1, 1]))],
651
           [TransferFunction(array([108.2]), array([1, 1])),
652
            TransferFunction(array([-109.6]), array([1, 1]))]], dtype=object)
653
    """
654
    tf_split_lst = []
9✔
655
    for i_out in range(transfer_function.noutputs):
9✔
656
        row = []
9✔
657
        for i_in in range(transfer_function.ninputs):
9✔
658
            row.append(
9✔
659
                tf.TransferFunction(
660
                    transfer_function.num[i_out][i_in],
661
                    transfer_function.den[i_out][i_in],
662
                    dt=transfer_function.dt,
663
                )
664
            )
665
        tf_split_lst.append(row)
9✔
666
    return np.array(tf_split_lst, dtype=object)
9✔
667

668
def _ensure_tf(arraylike_or_tf, dt=None):
9✔
669
    """Convert an array-like to a transfer function.
670

671
    Parameters
672
    ----------
673
    arraylike_or_tf : TransferFunction or array_like
674
        Array-like or transfer function.
675
    dt : None, True or float, optional
676
        System timebase. 0 (default) indicates continuous
677
        time, True indicates discrete time with unspecified sampling
678
        time, positive number is discrete time with specified
679
        sampling time, None indicates unspecified timebase (either
680
        continuous or discrete time). If None, timestep is not validated.
681

682
    Returns
683
    -------
684
    TransferFunction
685
        Transfer function.
686

687
    Raises
688
    ------
689
    ValueError
690
        If input cannot be converted to a transfer function.
691
    ValueError
692
        If the timesteps do not match.
693
    """
694
    # If the input is already a transfer function, return it right away
695
    if isinstance(arraylike_or_tf, tf.TransferFunction):
9✔
696
        # If timesteps don't match, raise an exception
697
        if (dt is not None) and (arraylike_or_tf.dt != dt):
9✔
698
            raise ValueError(
9✔
699
                f"`arraylike_or_tf.dt={arraylike_or_tf.dt}` does not match "
700
                f"argument `dt={dt}`."
701
            )
702
        return arraylike_or_tf
9✔
703
    if np.ndim(arraylike_or_tf) > 2:
9✔
704
        raise ValueError(
9✔
705
            "Array-like must have less than two dimensions to be converted "
706
            "into a transfer function."
707
        )
708
    # If it's not, then convert it to a transfer function
709
    arraylike_3d = np.atleast_3d(arraylike_or_tf)
9✔
710
    try:
9✔
711
        tfn = tf.TransferFunction(
9✔
712
            arraylike_3d,
713
            np.ones_like(arraylike_3d),
714
            dt,
715
        )
716
    except TypeError:
9✔
717
        raise ValueError(
9✔
718
            "`arraylike_or_tf` must only contain array-likes or transfer "
719
            "functions."
720
        )
721
    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

© 2025 Coveralls, Inc