|
10 | 10 | negate
|
11 | 11 | feedback
|
12 | 12 | connect
|
| 13 | +combine_tf |
| 14 | +split_tf |
13 | 15 |
|
14 | 16 | """
|
15 | 17 |
|
|
63 | 65 | from . import xferfcn as tf
|
64 | 66 | from .iosys import InputOutputSystem
|
65 | 67 |
|
66 |
| -__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect'] |
| 68 | +__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect', |
| 69 | + 'combine_tf', 'split_tf'] |
67 | 70 |
|
68 | 71 |
|
69 | 72 | def series(sys1, *sysn, **kwargs):
|
@@ -507,3 +510,212 @@ def connect(sys, Q, inputv, outputv):
|
507 | 510 | Ytrim[i,y-1] = 1.
|
508 | 511 |
|
509 | 512 | return Ytrim * sys * Utrim
|
| 513 | + |
| 514 | +def combine_tf(tf_array): |
| 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 = [] |
| 567 | + try: |
| 568 | + for row in tf_array: |
| 569 | + for tfn in row: |
| 570 | + dt_list.append(getattr(tfn, "dt", None)) |
| 571 | + except OSError: |
| 572 | + raise ValueError("`tf_array` has too few dimensions.") |
| 573 | + dt_set = set(dt_list) |
| 574 | + dt_set.discard(None) |
| 575 | + if len(dt_set) > 1: |
| 576 | + raise ValueError("Timesteps of transfer functions are " |
| 577 | + f"mismatched: {dt_set}") |
| 578 | + elif len(dt_set) == 0: |
| 579 | + dt = None |
| 580 | + else: |
| 581 | + dt = dt_set.pop() |
| 582 | + # Convert all entries to transfer function objects |
| 583 | + ensured_tf_array = [] |
| 584 | + for row in tf_array: |
| 585 | + ensured_row = [] |
| 586 | + for tfn in row: |
| 587 | + ensured_row.append(_ensure_tf(tfn, dt)) |
| 588 | + ensured_tf_array.append(ensured_row) |
| 589 | + # Iterate over |
| 590 | + num = [] |
| 591 | + den = [] |
| 592 | + for row_index, row in enumerate(ensured_tf_array): |
| 593 | + for j_out in range(row[0].noutputs): |
| 594 | + num_row = [] |
| 595 | + den_row = [] |
| 596 | + for col in row: |
| 597 | + if col.noutputs != row[0].noutputs: |
| 598 | + raise ValueError( |
| 599 | + "Mismatched number of transfer function outputs in " |
| 600 | + f"row {row_index}." |
| 601 | + ) |
| 602 | + for j_in in range(col.ninputs): |
| 603 | + num_row.append(col.num[j_out][j_in]) |
| 604 | + den_row.append(col.den[j_out][j_in]) |
| 605 | + num.append(num_row) |
| 606 | + den.append(den_row) |
| 607 | + for row_index, row in enumerate(num): |
| 608 | + if len(row) != len(num[0]): |
| 609 | + raise ValueError( |
| 610 | + "Mismatched number transfer function inputs in row " |
| 611 | + f"{row_index} of numerator." |
| 612 | + ) |
| 613 | + for row_index, row in enumerate(den): |
| 614 | + if len(row) != len(den[0]): |
| 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) |
| 620 | + |
| 621 | +def split_tf(transfer_function): |
| 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 = [] |
| 655 | + for i_out in range(transfer_function.noutputs): |
| 656 | + row = [] |
| 657 | + for i_in in range(transfer_function.ninputs): |
| 658 | + row.append( |
| 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) |
| 666 | + return np.array(tf_split_lst, dtype=object) |
| 667 | + |
| 668 | +def _ensure_tf(arraylike_or_tf, dt=None): |
| 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): |
| 696 | + # If timesteps don't match, raise an exception |
| 697 | + if (dt is not None) and (arraylike_or_tf.dt != dt): |
| 698 | + raise ValueError( |
| 699 | + f"`arraylike_or_tf.dt={arraylike_or_tf.dt}` does not match " |
| 700 | + f"argument `dt={dt}`." |
| 701 | + ) |
| 702 | + return arraylike_or_tf |
| 703 | + if np.ndim(arraylike_or_tf) > 2: |
| 704 | + raise ValueError( |
| 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) |
| 710 | + try: |
| 711 | + tfn = tf.TransferFunction( |
| 712 | + arraylike_3d, |
| 713 | + np.ones_like(arraylike_3d), |
| 714 | + dt, |
| 715 | + ) |
| 716 | + except TypeError: |
| 717 | + raise ValueError( |
| 718 | + "`arraylike_or_tf` must only contain array-likes or transfer " |
| 719 | + "functions." |
| 720 | + ) |
| 721 | + return tfn |
0 commit comments