Skip to content

Commit da55654

Browse files
authored
Merge pull request #1073 from sdahdah/main
Add `combine_tf()` and `split_tf()` functions for transfer matrices
2 parents 12dda4e + 1cc84a7 commit da55654

File tree

2 files changed

+768
-1
lines changed

2 files changed

+768
-1
lines changed

control/bdalg.py

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
negate
1111
feedback
1212
connect
13+
combine_tf
14+
split_tf
1315
1416
"""
1517

@@ -63,7 +65,8 @@
6365
from . import xferfcn as tf
6466
from .iosys import InputOutputSystem
6567

66-
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect']
68+
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect',
69+
'combine_tf', 'split_tf']
6770

6871

6972
def series(sys1, *sysn, **kwargs):
@@ -507,3 +510,212 @@ def connect(sys, Q, inputv, outputv):
507510
Ytrim[i,y-1] = 1.
508511

509512
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

Comments
 (0)