Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
iosys.py cleanup plus related subclass, docstring mods
  • Loading branch information
murrayrm committed Jun 19, 2023
commit 7273e11f9942fb0f660c679fd622388ba40f1981
3 changes: 0 additions & 3 deletions control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ def reset_defaults():
from .statesp import _statesp_defaults
defaults.update(_statesp_defaults)

from .nlsys import _iosys_defaults
defaults.update(_iosys_defaults)

from .optimal import _optimal_defaults
defaults.update(_optimal_defaults)

Expand Down
4 changes: 0 additions & 4 deletions control/frdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,6 @@ class FrequencyResponseData(LTI):

"""

# Allow NDarray * StateSpace to give StateSpace._rmul_() priority
# https://docs.scipy.org/doc/numpy/reference/arrays.classes.html
__array_priority__ = 13 # override ndarray, StateSpace, I/O sys

#
# Class attributes
#
Expand Down
62 changes: 17 additions & 45 deletions control/iosys.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# iosys.py - I/O system class and helper functions
# RMM, 13 Mar 2022
#
# This file implements the InputOutputSystem class, which is used as a parent
# class for FrequencyResponseData, InputOutputSystem, LTI, TimeResponseData,
# and other similar classes to allow naming of signals.
# This file implements the InputOutputSystem class, which is used as a
# parent class for StateSpace, TransferFunction, NonlinearIOSystem, LTI,
# FrequencyResponseData, InterconnectedSystem and other similar classes
# that allow naming of signals.

import numpy as np
from copy import deepcopy
Expand All @@ -12,7 +13,7 @@
from . import config

__all__ = ['InputOutputSystem', 'issiso', 'timebase', 'common_timebase',
'timebaseEqual', 'isdtime', 'isctime']
'isdtime', 'isctime']

# Define module default parameter values
_iosys_defaults = {
Expand All @@ -38,6 +39,15 @@ class InputOutputSystem(object):
a set of subclasses that are used to implement specific structures and
operations for different types of input/output dynamical systems.

The timebase for the system, dt, is used to specify whether the system
is operating in continuous or discrete time. It can have the following
values:

* dt = None No timebase specified
* dt = 0 Continuous time system
* dt > 0 Discrete time system with sampling time dt
* dt = True Discrete time system with unspecified sampling time

Parameters
----------
inputs : int, list of str, or None
Expand Down Expand Up @@ -93,22 +103,10 @@ class InputOutputSystem(object):
state_prefix : string, optional
Set the prefix for state signals. Default = 'x'.

Notes
-----
The :class:`~control.InputOuputSystem` class (and its subclasses) makes
use of two special methods for implementing much of the work of the class:

* _rhs(t, x, u): compute the right hand side of the differential or
difference equation for the system. This must be specified by the
subclass for the system.

* _out(t, x, u): compute the output for the current state of the system.
The default is to return the entire system state.

"""

# Allow ndarray * InputOutputSystem to give IOSystem._rmul_() priority
__array_priority__ = 13 # override ndarray, SS, TF types
# Allow NDarray * IOSystem to give IOSystem._rmul_() priority
# https://docs.scipy.org/doc/numpy/reference/arrays.classes.html
__array_priority__ = 20

def __init__(
self, name=None, inputs=None, outputs=None, states=None,
Expand Down Expand Up @@ -503,32 +501,6 @@ def common_timebase(dt1, dt2):
else:
raise ValueError("Systems have incompatible timebases")

# Check to see if two timebases are equal
Comment thread
murrayrm marked this conversation as resolved.
def timebaseEqual(sys1, sys2):
"""
Check to see if two systems have the same timebase

timebaseEqual(sys1, sys2)

returns True if the timebases for the two systems are compatible. By
default, systems with timebase 'None' are compatible with either
discrete or continuous timebase systems. If two systems have a discrete
timebase (dt > 0) then their timebases must be equal.
"""
warn("timebaseEqual will be deprecated in a future release of "
"python-control; use :func:`common_timebase` instead",
PendingDeprecationWarning)

if (type(sys1.dt) == bool or type(sys2.dt) == bool):
# Make sure both are unspecified discrete timebases
return type(sys1.dt) == type(sys2.dt) and sys1.dt == sys2.dt
elif (sys1.dt is None or sys2.dt is None):
# One or the other is unspecified => the other can be anything
return True
else:
return sys1.dt == sys2.dt


# Check to see if a system is a discrete time system
def isdtime(sys, strict=False):
"""
Expand Down
10 changes: 0 additions & 10 deletions control/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,13 @@ class LTI(InputOutputSystem):
contains the number of inputs and outputs, and the timebase (dt) for the
system. This function is not generally called directly by the user.

The timebase for the system, dt, is used to specify whether the system
is operating in continuous or discrete time. It can have the following
values:

* dt = None No timebase specified
* dt = 0 Continuous time system
* dt > 0 Discrete time system with sampling time dt
* dt = True Discrete time system with unspecified sampling time

When two LTI systems are combined, their timebases much match. A system
with timebase None can be combined with a system having a specified
timebase, and the result will have the timebase of the latter system.

Note: dt processing has been moved to the InputOutputSystem class.

"""

def __init__(self, inputs=1, outputs=1, states=None, name=None, **kwargs):
"""Assign the LTI object's numbers of inputs and ouputs."""
super().__init__(
Expand Down
73 changes: 36 additions & 37 deletions control/nlsys.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# nlsys.py - input/output system module
#
# RMM, 28 April 2019
#
# Additional features to add
Expand All @@ -19,13 +18,6 @@

"""

__author__ = "Richard Murray"
__copyright__ = "Copyright 2019, California Institute of Technology"
__credits__ = ["Richard Murray"]
__license__ = "BSD"
__maintainer__ = "Richard Murray"
__email__ = "murray@cds.caltech.edu"

import numpy as np
import scipy as sp
import copy
Expand All @@ -41,9 +33,6 @@
'input_output_response', 'find_eqpt', 'linearize',
'interconnect']

# Define module default parameter values
_iosys_defaults = {}


class NonlinearIOSystem(InputOutputSystem):
"""Nonlinear I/O system.
Expand Down Expand Up @@ -110,10 +99,19 @@ class NonlinearIOSystem(InputOutputSystem):
--------
InputOutputSystem : Input/output system class.

"""
# Set priority for operators
__array_priority__ = 13 # override ndarray, SS and TF types
Notes
-----
The :class:`~control.InputOuputSystem` class (and its subclasses) makes
use of two special methods for implementing much of the work of the class:

* _rhs(t, x, u): compute the right hand side of the differential or
difference equation for the system. If not specified, the system
has no state.

* _out(t, x, u): compute the output for the current state of the system.
The default is to return the entire system state.

"""
def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
"""Create a nonlinear I/O system given update and output functions."""
# Process keyword arguments
Expand All @@ -135,8 +133,9 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
if self.nstates is None:
self.nstates = 0
else:
raise ValueError("States specified but no update function "
"given.")
raise ValueError(
"states specified but no update function given.")

if outfcn is None:
# No output function specified => outputs = states
if self.noutputs is None and self.nstates is not None:
Expand All @@ -145,7 +144,7 @@ def __init__(self, updfcn, outfcn=None, params=None, **kwargs):
# Number of outputs = number of states => all is OK
pass
elif self.noutputs is not None and self.noutputs != 0:
raise ValueError("Outputs specified but no output function "
raise ValueError("outputs specified but no output function "
"(and nstates not known).")

# Initialize current parameters to default parameters
Expand Down Expand Up @@ -266,7 +265,7 @@ def __add__(self, other):
# Make sure number of input and outputs match
if self.ninputs != other.ninputs or self.noutputs != other.noutputs:
raise ValueError("Can't add systems with incompatible numbers of "
"inputs or outputs.")
"inputs or outputs")

# Create a new system to handle the composition
inplist = [[(0, i), (1, i)] for i in range(self.ninputs)]
Expand All @@ -286,8 +285,8 @@ def __radd__(self, other):

# Make sure number of input and outputs match
if self.ninputs != other.ninputs or self.noutputs != other.noutputs:
raise ValueError("Can't add systems with incompatible numbers of "
"inputs or outputs.")
raise ValueError("can't add systems with incompatible numbers of "
"inputs or outputs")

# Create a new system to handle the composition
inplist = [[(0, i), (1, i)] for i in range(other.ninputs)]
Expand All @@ -308,8 +307,8 @@ def __sub__(self, other):
# Make sure number of input and outputs match
if self.ninputs != other.ninputs or self.noutputs != other.noutputs:
raise ValueError(
"Can't substract systems with incompatible numbers of "
"inputs or outputs.")
"can't substract systems with incompatible numbers of "
"inputs or outputs")
ninputs = self.ninputs
noutputs = self.noutputs

Expand Down Expand Up @@ -613,7 +612,7 @@ def __init__(self, syslist, connections=None, inplist=None, outlist=None,
name, inputs, outputs, states, _ = _process_iosys_keywords(kwargs)

# Initialize the system list and index
self.syslist = list(syslist) # insure modifications can be made
self.syslist = list(syslist) # ensure modifications can be made
self.syslist_index = {}

# Initialize the input, output, and state counts, indices
Expand All @@ -638,7 +637,7 @@ def __init__(self, syslist, connections=None, inplist=None, outlist=None,
# Make sure number of inputs, outputs, states is given
if sys.ninputs is None or sys.noutputs is None or \
sys.nstates is None:
raise TypeError("System '%s' must define number of inputs, "
raise TypeError("system '%s' must define number of inputs, "
"outputs, states in order to be connected" %
sys.name)

Expand Down Expand Up @@ -734,7 +733,7 @@ def outfcn(t, x, u, params):
input_indices, output_indices):
if self.connect_map[input_index, output_index] != 0:
warn("multiple connections given for input %d" %
input_index + ". Combining with previous entries.")
input_index + "; combining with previous entries")
self.connect_map[input_index, output_index] += gain

# Convert the input list to a matrix: maps system to subsystems
Expand All @@ -744,13 +743,13 @@ def outfcn(t, x, u, params):
inpspec = [inpspec]
if not isinstance(inpspec, list):
raise ValueError("specifications in inplist must be of type "
"int, str, tuple or list.")
"int, str, tuple or list")
for spec in inpspec:
ulist_indices = self._parse_input_spec(spec)
for j, ulist_index in enumerate(ulist_indices):
if self.input_map[ulist_index, index] != 0:
warn("multiple connections given for input %d" %
index + ". Combining with previous entries.")
index + "; combining with previous entries.")
self.input_map[ulist_index, index + j] += 1

# Convert the output list to a matrix: maps subsystems to system
Expand All @@ -760,13 +759,13 @@ def outfcn(t, x, u, params):
outspec = [outspec]
if not isinstance(outspec, list):
raise ValueError("specifications in outlist must be of type "
"int, str, tuple or list.")
"int, str, tuple or list")
for spec in outspec:
ylist_indices, gain = self._parse_output_spec(spec)
for j, ylist_index in enumerate(ylist_indices):
if self.output_map[index, ylist_index] != 0:
warn("multiple connections given for output %d" %
index + ". Combining with previous entries.")
index + "; combining with previous entries")
self.output_map[index + j, ylist_index] += gain

def _update_params(self, params, warning=False):
Expand Down Expand Up @@ -866,7 +865,7 @@ def _compute_static_io(self, t, x, u):

# Make sure that we stopped before detecting an algebraic loop
if cycle_count == 0:
raise RuntimeError("Algebraic loop detected.")
raise RuntimeError("algebraic loop detected")

return ulist, ylist

Expand All @@ -876,7 +875,7 @@ def _parse_input_spec(self, spec):
subsys_index, input_indices, gain = _parse_spec(
self.syslist, spec, 'input')
if gain != 1:
raise ValueError("gain not allowed in spec '%s'." % str(spec))
raise ValueError("gain not allowed in spec '%s'" % str(spec))

# Return the indices into the input vector list (ylist)
return [self.input_offset[subsys_index] + i for i in input_indices]
Expand Down Expand Up @@ -1431,8 +1430,8 @@ def ivp_rhs(t, x):
# Make sure the time vector is uniformly spaced
dt = t_eval[1] - t_eval[0]
if not np.allclose(t_eval[1:] - t_eval[:-1], dt):
raise ValueError("Parameter ``t_eval``: time values must be "
"equally spaced.")
raise ValueError("parameter ``t_eval``: time values must be "
"equally spaced")

# Make sure the sample time matches the given time
if sys.dt is not True:
Expand Down Expand Up @@ -1579,7 +1578,7 @@ def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None,
(u0 is not None and len(u0) != ninputs) or \
(y0 is not None and len(y0) != noutputs) or \
(dx0 is not None and len(dx0) != nstates):
raise ValueError("Length of input arguments does not match system.")
raise ValueError("length of input arguments does not match system")

# Update the parameter values
sys._update_params(params)
Expand Down Expand Up @@ -1691,8 +1690,8 @@ def rootfun(z):
num_freedoms = len(state_vars) + len(input_vars)
num_constraints = len(output_vars) + len(deriv_vars)
if num_constraints != num_freedoms:
warn("Number of constraints (%d) does not match number of degrees "
"of freedom (%d). Results may be meaningless." %
warn("number of constraints (%d) does not match number of degrees "
"of freedom (%d); results may be meaningless" %
(num_constraints, num_freedoms))

# Make copies of the state and input variables to avoid overwriting
Expand Down Expand Up @@ -1823,7 +1822,7 @@ def _find_size(sysval, vecval):
elif sysval == 1:
# (1, scalar) is also a valid combination from legacy code
return 1
raise ValueError("Can't determine size of system component.")
raise ValueError("can't determine size of system component")


# Function to create an interconnected system
Expand Down
9 changes: 1 addition & 8 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,6 @@ class StateSpace(NonlinearIOSystem, LTI):
The default value of dt can be changed by changing the value of
``control.config.defaults['control.default_dt']``.

Note: timebase processing has moved to iosys.

A state space system is callable and returns the value of the transfer
function evaluated at a point in the complex plane. See
:meth:`~control.StateSpace.__call__` for a more detailed description.
Expand All @@ -172,10 +170,6 @@ class StateSpace(NonlinearIOSystem, LTI):
`'separate'`, the matrices are shown separately.

"""

# Allow ndarray * StateSpace to give StateSpace._rmul_() priority
__array_priority__ = 12 # override ndarray and TF types

def __init__(self, *args, **kwargs):
"""StateSpace(A, B, C, D[, dt])

Expand Down Expand Up @@ -278,9 +272,8 @@ def __init__(self, *args, **kwargs):
updfcn, outfcn,
name=name, inputs=inputs, outputs=outputs,
states=states, dt=dt, **kwargs)
self.params = {}

# Reset shapes (may not be needed once np.matrix support is removed)
# Reset shapes if the system is static
if self._isstatic():
A.shape = (0, 0)
B.shape = (0, self.ninputs)
Expand Down
Loading