- Notifications
You must be signed in to change notification settings - Fork441
add summing junction + implicit signal interconnection#517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
murrayrm merged 3 commits intopython-control:masterfrommurrayrm:sumblk_implicit_interconnectJan 26, 2021
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
3 commits Select commitHold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
240 changes: 223 additions & 17 deletionscontrol/iosys.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -3,16 +3,11 @@ | ||
# RMM, 28 April 2019 | ||
# | ||
# Additional features to add | ||
# * Allow constant inputs for MIMO input_output_response (w/out ones) | ||
# * Add support for constants/matrices as part of operators (1 + P) | ||
# * Add unit tests (and example?) for time-varying systems | ||
# * Allow time vector for discrete time simulations to be multiples of dt | ||
# * Check the way initial outputs for discrete time systems are handled | ||
# | ||
"""The :mod:`~control.iosys` module contains the | ||
@@ -36,14 +31,15 @@ | ||
import copy | ||
from warnings import warn | ||
from .statesp import StateSpace, tf2ss, _convert_to_statespace | ||
from .timeresp import _check_convert_array, _process_time_response | ||
from .lti import isctime, isdtime, common_timebase | ||
from . import config | ||
__all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', | ||
'InterconnectedSystem', 'LinearICSystem', 'input_output_response', | ||
'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect', | ||
'summing_junction'] | ||
# Define module default parameter values | ||
_iosys_defaults = { | ||
@@ -481,9 +477,14 @@ def feedback(self, other=1, sign=-1, params={}): | ||
""" | ||
# TODO: add conversion to I/O system when needed | ||
if not isinstance(other, InputOutputSystem): | ||
# Try converting to a state space system | ||
try: | ||
other = _convert_to_statespace(other) | ||
except TypeError: | ||
raise TypeError( | ||
"Feedback around I/O system must be an I/O system " | ||
"or convertable to an I/O system.") | ||
other = LinearIOSystem(other) | ||
# Make sure systems can be interconnected | ||
if self.noutputs != other.ninputs or other.noutputs != self.ninputs: | ||
@@ -1846,7 +1847,7 @@ def tf2io(*args, **kwargs): | ||
# Function to create an interconnected system | ||
def interconnect(syslist, connections=None, inplist=[], outlist=[], | ||
inputs=None, outputs=None, states=None, | ||
params={}, dt=None, name=None): | ||
"""Interconnect a set of input/output systems. | ||
@@ -1893,8 +1894,18 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], | ||
and the special form '-sys.sig' can be used to specify a signal with | ||
gain -1. | ||
If omitted, the `interconnect` function will attempt to create the | ||
interconnection map by connecting all signals with the same base names | ||
(ignoring the system name). Specifically, for each input signal name | ||
in the list of systems, if that signal name corresponds to the output | ||
signal in any of the systems, it will be connected to that input (with | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Nice that this summation effect is stated explicitly. | ||
a summation across all signals if the output name occurs in more than | ||
one system). | ||
The `connections` keyword can also be set to `False`, which will leave | ||
the connection map empty and it can be specified instead using the | ||
low-level :func:`~control.InterconnectedSystem.set_connect_map` | ||
method. | ||
inplist : list of input connections, optional | ||
List of connections for how the inputs for the overall system are | ||
@@ -1966,24 +1977,33 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], | ||
Example | ||
------- | ||
>>> P = control.LinearIOSystem( | ||
>>>control.rss(2, 2, 2, strictly_proper=True), name='P') | ||
>>> C = control.LinearIOSystem(control.rss(2, 2, 2), name='C') | ||
>>>T = control.interconnect( | ||
>>> [P, C], | ||
>>> connections = [ | ||
>>> ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[1]'], | ||
>>> ['C.u[0]', '-P.y[0]'], ['C.u[1]', '-P.y[1]']], | ||
>>> inplist = ['C.u[0]', 'C.u[1]'], | ||
>>> outlist = ['P.y[0]', 'P.y[1]'], | ||
>>> ) | ||
For a SISO system, this example can be simplified by using the | ||
:func:`~control.summing_block` function and the ability to automatically | ||
interconnect signals with the same names: | ||
>>> P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y') | ||
>>> C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u') | ||
>>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') | ||
>>> T = control.interconnect([P, C, sumblk], inplist='r', outlist='y') | ||
Notes | ||
----- | ||
If a system is duplicated in the list of systems to be connected, | ||
a warning is generated a copy of the system is created with the | ||
name of the new system determined by adding the prefix and suffix | ||
strings in config.defaults['iosys.linearized_system_name_prefix'] | ||
and config.defaults['iosys.linearized_system_name_suffix'], with the | ||
default being to add the suffix '$copy'$ to the system name. | ||
It is possible to replace lists in most of arguments with tuples instead, | ||
@@ -2001,6 +2021,78 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], | ||
:class:`~control.InputOutputSystem`. | ||
""" | ||
# If connections was not specified, set up default connection list | ||
if connections is None: | ||
# For each system input, look for outputs with the same name | ||
connections = [] | ||
for input_sys in syslist: | ||
for input_name in input_sys.input_index.keys(): | ||
connect = [input_sys.name + "." + input_name] | ||
for output_sys in syslist: | ||
if input_name in output_sys.output_index.keys(): | ||
connect.append(output_sys.name + "." + input_name) | ||
if len(connect) > 1: | ||
connections.append(connect) | ||
elif connections is False: | ||
# Use an empty connections list | ||
connections = [] | ||
# Process input list | ||
if not isinstance(inplist, (list, tuple)): | ||
inplist = [inplist] | ||
new_inplist = [] | ||
for signal in inplist: | ||
# Check for signal names without a system name | ||
if isinstance(signal, str) and len(signal.split('.')) == 1: | ||
# Get the signal name | ||
name = signal[1:] if signal[0] == '-' else signal | ||
sign = '-' if signal[0] == '-' else "" | ||
# Look for the signal name as a system input | ||
new_name = None | ||
for sys in syslist: | ||
if name in sys.input_index.keys(): | ||
if new_name is not None: | ||
raise ValueError("signal %s is not unique" % name) | ||
new_name = sign + sys.name + "." + name | ||
# Make sure we found the name | ||
if new_name is None: | ||
raise ValueError("could not find signal %s" % name) | ||
else: | ||
new_inplist.append(new_name) | ||
else: | ||
new_inplist.append(signal) | ||
inplist = new_inplist | ||
# Process output list | ||
if not isinstance(outlist, (list, tuple)): | ||
outlist = [outlist] | ||
new_outlist = [] | ||
for signal in outlist: | ||
# Check for signal names without a system name | ||
if isinstance(signal, str) and len(signal.split('.')) == 1: | ||
# Get the signal name | ||
name = signal[1:] if signal[0] == '-' else signal | ||
sign = '-' if signal[0] == '-' else "" | ||
# Look for the signal name as a system output | ||
new_name = None | ||
for sys in syslist: | ||
if name in sys.output_index.keys(): | ||
if new_name is not None: | ||
raise ValueError("signal %s is not unique" % name) | ||
new_name = sign + sys.name + "." + name | ||
# Make sure we found the name | ||
if new_name is None: | ||
raise ValueError("could not find signal %s" % name) | ||
else: | ||
new_outlist.append(new_name) | ||
else: | ||
new_outlist.append(signal) | ||
outlist = new_outlist | ||
newsys = InterconnectedSystem( | ||
syslist, connections=connections, inplist=inplist, outlist=outlist, | ||
inputs=inputs, outputs=outputs, states=states, | ||
@@ -2011,3 +2103,117 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], | ||
return LinearICSystem(newsys, None) | ||
return newsys | ||
# Summing junction | ||
def summing_junction(inputs, output='y', dimension=None, name=None, prefix='u'): | ||
"""Create a summing junction as an input/output system. | ||
This function creates a static input/output system that outputs the sum of | ||
the inputs, potentially with a change in sign for each individual input. | ||
The input/output system that is created by this function can be used as a | ||
component in the :func:`~control.interconnect` function. | ||
Parameters | ||
---------- | ||
inputs : int, string or list of strings | ||
Description of the inputs to the summing junction. This can be given | ||
as an integer count, a string, or a list of strings. If an integer | ||
count is specified, the names of the input signals will be of the form | ||
`u[i]`. | ||
output : string, optional | ||
Name of the system output. If not specified, the output will be 'y'. | ||
dimension : int, optional | ||
The dimension of the summing junction. If the dimension is set to a | ||
positive integer, a multi-input, multi-output summing junction will be | ||
created. The input and output signal names will be of the form | ||
`<signal>[i]` where `signal` is the input/output signal name specified | ||
by the `inputs` and `output` keywords. Default value is `None`. | ||
name : string, optional | ||
System name (used for specifying signals). If unspecified, a generic | ||
name <sys[id]> is generated with a unique integer id. | ||
prefix : string, optional | ||
If `inputs` is an integer, create the names of the states using the | ||
given prefix (default = 'u'). The names of the input will be of the | ||
form `prefix[i]`. | ||
Returns | ||
------- | ||
sys : static LinearIOSystem | ||
Linear input/output system object with no states and only a direct | ||
term that implements the summing junction. | ||
Example | ||
------- | ||
>>> P = control.tf2io(ct.tf(1, [1, 0]), inputs='u', outputs='y') | ||
>>> C = control.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='u') | ||
>>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') | ||
>>> T = control.interconnect((P, C, sumblk), inplist='r', outlist='y') | ||
""" | ||
# Utility function to parse input and output signal lists | ||
def _parse_list(signals, signame='input', prefix='u'): | ||
# Parse signals, including gains | ||
if isinstance(signals, int): | ||
nsignals = signals | ||
names = ["%s[%d]" % (prefix, i) for i in range(nsignals)] | ||
gains = np.ones((nsignals,)) | ||
elif isinstance(signals, str): | ||
nsignals = 1 | ||
gains = [-1 if signals[0] == '-' else 1] | ||
names = [signals[1:] if signals[0] == '-' else signals] | ||
elif isinstance(signals, list) and \ | ||
all([isinstance(x, str) for x in signals]): | ||
nsignals = len(signals) | ||
gains = np.ones((nsignals,)) | ||
names = [] | ||
for i in range(nsignals): | ||
if signals[i][0] == '-': | ||
gains[i] = -1 | ||
names.append(signals[i][1:]) | ||
else: | ||
names.append(signals[i]) | ||
else: | ||
raise ValueError( | ||
"could not parse %s description '%s'" | ||
% (signame, str(signals))) | ||
# Return the parsed list | ||
return nsignals, names, gains | ||
# Read the input list | ||
ninputs, input_names, input_gains = _parse_list( | ||
inputs, signame="input", prefix=prefix) | ||
noutputs, output_names, output_gains = _parse_list( | ||
output, signame="output", prefix='y') | ||
if noutputs > 1: | ||
raise NotImplementedError("vector outputs not yet supported") | ||
# If the dimension keyword is present, vectorize inputs and outputs | ||
if isinstance(dimension, int) and dimension >= 1: | ||
# Create a new list of input/output names and update parameters | ||
input_names = ["%s[%d]" % (name, dim) | ||
for name in input_names | ||
for dim in range(dimension)] | ||
ninputs = ninputs * dimension | ||
output_names = ["%s[%d]" % (name, dim) | ||
for name in output_names | ||
for dim in range(dimension)] | ||
noutputs = noutputs * dimension | ||
elif dimension is not None: | ||
raise ValueError( | ||
"unrecognized dimension value '%s'" % str(dimension)) | ||
else: | ||
dimension = 1 | ||
# Create the direct term | ||
D = np.kron(input_gains * output_gains[0], np.eye(dimension)) | ||
# Create a linear system of the appropriate size | ||
ss_sys = StateSpace( | ||
np.zeros((0, 0)), np.ones((0, ninputs)), np.ones((noutputs, 0)), D) | ||
# Create a LinearIOSystem | ||
return LinearIOSystem( | ||
ss_sys, inputs=input_names, outputs=output_names, name=name) |
4 changes: 2 additions & 2 deletionscontrol/statesp.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.