Skip to content

Small improvements to nlsys, bdalg #1019

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 9, 2024
129 changes: 104 additions & 25 deletions control/bdalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@
"""

from functools import reduce
import numpy as np
from warnings import warn
from . import xferfcn as tf
from . import statesp as ss

import numpy as np

from . import frdata as frd
from . import statesp as ss
from . import xferfcn as tf
from .iosys import InputOutputSystem

__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect']


def series(sys1, *sysn):
def series(sys1, *sysn, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kwargs not documented in docstring, here or the other functions in bdalg.py.

r"""series(sys1, sys2, [..., sysn])

Return the series connection (`sysn` \* ...\ \*) `sys2` \* `sys1`.
Expand All @@ -79,6 +81,20 @@ def series(sys1, *sysn):
out : scalar, array, or :class:`InputOutputSystem`
Series interconnection of the systems.

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

Raises
------
ValueError
Expand Down Expand Up @@ -117,10 +133,12 @@ def series(sys1, *sysn):
(2, 1, 5)

"""
return reduce(lambda x, y: y * x, sysn, sys1)
sys = reduce(lambda x, y: y * x, sysn, sys1)
sys.update_names(**kwargs)
return sys


def parallel(sys1, *sysn):
def parallel(sys1, *sysn, **kwargs):
r"""parallel(sys1, sys2, [..., sysn])

Return the parallel connection `sys1` + `sys2` (+ ...\ + `sysn`).
Expand All @@ -135,6 +153,20 @@ def parallel(sys1, *sysn):
out : scalar, array, or :class:`InputOutputSystem`
Parallel interconnection of the systems.

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

Raises
------
ValueError
Expand Down Expand Up @@ -171,10 +203,11 @@ def parallel(sys1, *sysn):
(3, 4, 7)

"""
return reduce(lambda x, y: x + y, sysn, sys1)

sys = reduce(lambda x, y: x + y, sysn, sys1)
sys.update_names(**kwargs)
return sys

def negate(sys):
def negate(sys, **kwargs):
"""
Return the negative of a system.

Expand All @@ -188,15 +221,29 @@ def negate(sys):
out : scalar, array, or :class:`InputOutputSystem`
Negated system.

Notes
-----
This function is a wrapper for the __neg__ function in the StateSpace and
TransferFunction classes. The output type is the same as the input type.
Other Parameters
----------------
inputs, outputs : str, or list of str, optional
List of strings that name the individual signals. If not given,
signal names will be of the form `s[i]` (where `s` is one of `u`,
or `y`). See :class:`InputOutputSystem` for more information.
states : str, or list of str, optional
List of names for system states. If not given, state names will be
of of the form `x[i]` for interconnections of linear systems or
'<subsys_name>.<state_name>' for interconnected nonlinear systems.
name : string, optional
System name (used for specifying signals). If unspecified, a generic
name <sys[id]> is generated with a unique integer id.

See Also
--------
append, feedback, interconnect, parallel, series

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

Examples
--------
>>> G = ct.tf([2], [1, 1])
Expand All @@ -208,11 +255,12 @@ def negate(sys):
np.float64(-2.0)

"""
return -sys
sys = -sys
sys.update_names(**kwargs)
return sys

#! TODO: expand to allow sys2 default to work in MIMO case?
#! TODO: allow renaming of signals (for all bdalg operations)
def feedback(sys1, sys2=1, sign=-1):
def feedback(sys1, sys2=1, sign=-1, **kwargs):
"""Feedback interconnection between two I/O systems.

Parameters
Expand All @@ -229,6 +277,20 @@ def feedback(sys1, sys2=1, sign=-1):
out : scalar, array, or :class:`InputOutputSystem`
Feedback interconnection of the systems.

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

Raises
------
ValueError
Expand Down Expand Up @@ -261,7 +323,7 @@ def feedback(sys1, sys2=1, sign=-1):
# Allow anything with a feedback function to call that function
# TODO: rewrite to allow __rfeedback__
try:
return sys1.feedback(sys2, sign)
return sys1.feedback(sys2, sign, **kwargs)
except (AttributeError, TypeError):
pass

Expand All @@ -284,9 +346,11 @@ def feedback(sys1, sys2=1, sign=-1):
else:
sys1 = ss._convert_to_statespace(sys1)

return sys1.feedback(sys2, sign)
sys = sys1.feedback(sys2, sign)
sys.update_names(**kwargs)
return sys

def append(*sys):
def append(*sys, **kwargs):
"""append(sys1, sys2, [..., sysn])

Group LTI state space models by appending their inputs and outputs.
Expand All @@ -299,6 +363,20 @@ def append(*sys):
sys1, sys2, ..., sysn: scalar, array, or :class:`StateSpace`
I/O systems to combine.

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

Returns
-------
out: :class:`StateSpace`
Expand Down Expand Up @@ -327,6 +405,7 @@ def append(*sys):
s1 = ss._convert_to_statespace(sys[0])
for s in sys[1:]:
s1 = s1.append(s)
s1.update_names(**kwargs)
return s1

def connect(sys, Q, inputv, outputv):
Expand Down Expand Up @@ -370,6 +449,12 @@ def connect(sys, Q, inputv, outputv):
--------
append, feedback, interconnect, negate, parallel, series

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

Examples
--------
>>> G = ct.rss(7, inputs=2, outputs=2)
Expand All @@ -378,12 +463,6 @@ def connect(sys, Q, inputv, outputv):
>>> T.ninputs, T.noutputs, T.nstates
(1, 2, 7)

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

"""
# TODO: maintain `connect` for use in MATLAB submodule (?)
warn("`connect` is deprecated; use `interconnect`", DeprecationWarning)
Expand Down
52 changes: 49 additions & 3 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
# FrequencyResponseData, InterconnectedSystem and other similar classes
# that allow naming of signals.

import numpy as np
import re
from copy import deepcopy
from warnings import warn
import re

import numpy as np

from . import config

__all__ = ['InputOutputSystem', 'issiso', 'timebase', 'common_timebase',
Expand Down Expand Up @@ -366,6 +368,51 @@ def find_states(self, name_list):
lambda self: list(self.state_index.keys()), # getter
set_states) # setter

# TODO: add dict as a means to selective change names? [GH #1019]
def update_names(self, **kwargs):
"""update_names([name, inputs, outputs, states])

Update signal and system names for an I/O system.

Parameters
----------
name : str, optional
New system name.
inputs : list of str, int, or None, optional
List of strings that name the individual input signals. If
given as an integer or None, signal names default to the form
`u[i]`. See :class:`InputOutputSystem` for more information.
outputs : list of str, int, or None, optional
Description of output signals; defaults to `y[i]`.
states : int, list of str, int, or None, optional
Description of system states; defaults to `x[i]`.

"""
self.name = kwargs.pop('name', self.name)
if 'inputs' in kwargs:
ninputs, input_index = _process_signal_list(
kwargs.pop('inputs'), prefix=kwargs.pop('input_prefix', 'u'))
if self.ninputs and self.ninputs != ninputs:
raise ValueError("number of inputs does not match system size")
self.input_index = input_index
if 'outputs' in kwargs:
noutputs, output_index = _process_signal_list(
kwargs.pop('outputs'), prefix=kwargs.pop('output_prefix', 'y'))
if self.noutputs and self.noutputs != noutputs:
raise ValueError("number of outputs does not match system size")
self.output_index = output_index
if 'states' in kwargs:
nstates, state_index = _process_signal_list(
kwargs.pop('states'), prefix=kwargs.pop('state_prefix', 'x'))
if self.nstates != nstates:
raise ValueError("number of states does not match system size")
self.state_index = state_index

# Make sure we processed all of the arguments
if kwargs:
raise TypeError("unrecognized keywords: ", str(kwargs))


def isctime(self, strict=False):
"""
Check to see if a system is a continuous-time system.
Expand Down Expand Up @@ -823,7 +870,6 @@ def _process_labels(labels, name, default):
# This function returns the subsystem index, a list of indices for the
# system signals, and the gain to use for that set of signals.
#
import re

def _parse_spec(syslist, spec, signame, dictname=None):
"""Parse a signal specification, returning system and signal index."""
Expand Down
Loading
Loading