Skip to content

TransferFunction array priority plus system type conversion checking #498

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
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions control/bdalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ def feedback(sys1, sys2=1, sign=-1):
if isinstance(sys2, tf.TransferFunction):
sys1 = tf._convert_to_transfer_function(sys1)
elif isinstance(sys2, ss.StateSpace):
sys1 = ss._convertToStateSpace(sys1)
sys1 = ss._convert_to_statespace(sys1)
elif isinstance(sys2, frd.FRD):
sys1 = frd._convertToFRD(sys1, sys2.omega)
sys1 = frd._convert_to_FRD(sys1, sys2.omega)
else: # sys2 is a scalar.
sys1 = tf._convert_to_transfer_function(sys1)
sys2 = tf._convert_to_transfer_function(sys2)
Expand Down
2 changes: 1 addition & 1 deletion control/dtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"""

from .lti import isctime
from .statesp import StateSpace, _convertToStateSpace
from .statesp import StateSpace

__all__ = ['sample_system', 'c2d']

Expand Down
18 changes: 9 additions & 9 deletions control/frdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def __add__(self, other):

# Convert the second argument to a frequency response function.
# or re-base the frd to the current omega (if needed)
other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

# Check that the input-output sizes are consistent.
if self.inputs != other.inputs:
Expand Down Expand Up @@ -232,7 +232,7 @@ def __mul__(self, other):
return FRD(self.fresp * other, self.omega,
smooth=(self.ifunc is not None))
else:
other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

# Check that the input-output sizes are consistent.
if self.inputs != other.outputs:
Expand All @@ -259,7 +259,7 @@ def __rmul__(self, other):
return FRD(self.fresp * other, self.omega,
smooth=(self.ifunc is not None))
else:
other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

# Check that the input-output sizes are consistent.
if self.outputs != other.inputs:
Expand Down Expand Up @@ -287,7 +287,7 @@ def __truediv__(self, other):
return FRD(self.fresp * (1/other), self.omega,
smooth=(self.ifunc is not None))
else:
other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

if (self.inputs > 1 or self.outputs > 1 or
other.inputs > 1 or other.outputs > 1):
Expand All @@ -310,7 +310,7 @@ def __rtruediv__(self, other):
return FRD(other / self.fresp, self.omega,
smooth=(self.ifunc is not None))
else:
other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

if (self.inputs > 1 or self.outputs > 1 or
other.inputs > 1 or other.outputs > 1):
Expand Down Expand Up @@ -450,7 +450,7 @@ def freqresp(self, omega):
def feedback(self, other=1, sign=-1):
"""Feedback interconnection between two FRD objects."""

other = _convertToFRD(other, omega=self.omega)
other = _convert_to_FRD(other, omega=self.omega)

if (self.outputs != other.inputs or self.inputs != other.outputs):
raise ValueError(
Expand Down Expand Up @@ -486,7 +486,7 @@ def feedback(self, other=1, sign=-1):
FRD = FrequencyResponseData


def _convertToFRD(sys, omega, inputs=1, outputs=1):
def _convert_to_FRD(sys, omega, inputs=1, outputs=1):
"""Convert a system to frequency response data form (if needed).

If sys is already an frd, and its frequency range matches or
Expand All @@ -496,8 +496,8 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
scalar, then the number of inputs and outputs can be specified
manually, as in:

>>> frd = _convertToFRD(3., omega) # Assumes inputs = outputs = 1
>>> frd = _convertToFRD(1., omegs, inputs=3, outputs=2)
>>> frd = _convert_to_FRD(3., omega) # Assumes inputs = outputs = 1
>>> frd = _convert_to_FRD(1., omegs, inputs=3, outputs=2)

In the latter example, sys's matrix transfer function is [[1., 1., 1.]
[1., 1., 1.]].
Expand Down
18 changes: 11 additions & 7 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def __mul__(sys2, sys1):
raise NotImplemented("Matrix multiplication not yet implemented")

elif not isinstance(sys1, InputOutputSystem):
raise ValueError("Unknown I/O system object ", sys1)
raise TypeError("Unknown I/O system object ", sys1)

# Make sure systems can be interconnected
if sys1.noutputs != sys2.ninputs:
Expand Down Expand Up @@ -254,20 +254,24 @@ def __mul__(sys2, sys1):

def __rmul__(sys1, sys2):
"""Pre-multiply an input/output systems by a scalar/matrix"""
if isinstance(sys2, (int, float, np.number)):
if isinstance(sys2, InputOutputSystem):
# Both systems are InputOutputSystems => use __mul__
return InputOutputSystem.__mul__(sys2, sys1)

elif isinstance(sys2, (int, float, np.number)):
# TODO: Scale the output
raise NotImplemented("Scalar multiplication not yet implemented")

elif isinstance(sys2, np.ndarray):
# TODO: Post-multiply by a matrix
raise NotImplemented("Matrix multiplication not yet implemented")

elif not isinstance(sys2, InputOutputSystem):
raise ValueError("Unknown I/O system object ", sys1)
elif isinstance(sys2, StateSpace):
# TODO: Should eventuall preserve LinearIOSystem structure
return StateSpace.__mul__(sys2, sys1)

else:
# Both systems are InputOutputSystems => use __mul__
return InputOutputSystem.__mul__(sys2, sys1)
raise TypeError("Unknown I/O system object ", sys1)

def __add__(sys1, sys2):
"""Add two input/output systems (parallel interconnection)"""
Expand All @@ -281,7 +285,7 @@ def __add__(sys1, sys2):
raise NotImplemented("Matrix addition not yet implemented")

elif not isinstance(sys2, InputOutputSystem):
raise ValueError("Unknown I/O system object ", sys2)
raise TypeError("Unknown I/O system object ", sys2)

# Make sure number of input and outputs match
if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs:
Expand Down
37 changes: 17 additions & 20 deletions control/statesp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

# Python 3 compatibility (needs to go here)
from __future__ import print_function
from __future__ import division # for _convertToStateSpace
from __future__ import division # for _convert_to_statespace

"""Copyright (c) 2010 by California Institute of Technology
All rights reserved.
Expand Down Expand Up @@ -527,7 +527,7 @@ def __add__(self, other):
D = self.D + other
dt = self.dt
else:
other = _convertToStateSpace(other)
other = _convert_to_statespace(other)

# Check to make sure the dimensions are OK
if ((self.inputs != other.inputs) or
Expand Down Expand Up @@ -577,7 +577,7 @@ def __mul__(self, other):
D = self.D * other
dt = self.dt
else:
other = _convertToStateSpace(other)
other = _convert_to_statespace(other)

# Check to make sure the dimensions are OK
if self.inputs != other.outputs:
Expand Down Expand Up @@ -614,7 +614,7 @@ def __rmul__(self, other):

# is lti, and convertible?
if isinstance(other, LTI):
return _convertToStateSpace(other) * self
return _convert_to_statespace(other) * self

# try to treat this as a matrix
try:
Expand Down Expand Up @@ -839,7 +839,7 @@ def zero(self):
def feedback(self, other=1, sign=-1):
"""Feedback interconnection between two LTI systems."""

other = _convertToStateSpace(other)
other = _convert_to_statespace(other)

# Check to make sure the dimensions are OK
if (self.inputs != other.outputs) or (self.outputs != other.inputs):
Expand Down Expand Up @@ -907,7 +907,7 @@ def lft(self, other, nu=-1, ny=-1):
Dimension of (plant) control input.

"""
other = _convertToStateSpace(other)
other = _convert_to_statespace(other)
# maximal values for nu, ny
if ny == -1:
ny = min(other.inputs, self.outputs)
Expand Down Expand Up @@ -1061,7 +1061,7 @@ def append(self, other):
The second model is converted to state-space if necessary, inputs and
outputs are appended and their order is preserved"""
if not isinstance(other, StateSpace):
other = _convertToStateSpace(other)
other = _convert_to_statespace(other)

self.dt = common_timebase(self.dt, other.dt)

Expand Down Expand Up @@ -1186,16 +1186,16 @@ def is_static_gain(self):


# TODO: add discrete time check
def _convertToStateSpace(sys, **kw):
def _convert_to_statespace(sys, **kw):
"""Convert a system to state space form (if needed).

If sys is already a state space, then it is returned. If sys is a
transfer function object, then it is converted to a state space and
returned. If sys is a scalar, then the number of inputs and outputs can
be specified manually, as in:

>>> sys = _convertToStateSpace(3.) # Assumes inputs = outputs = 1
>>> sys = _convertToStateSpace(1., inputs=3, outputs=2)
>>> sys = _convert_to_statespace(3.) # Assumes inputs = outputs = 1
>>> sys = _convert_to_statespace(1., inputs=3, outputs=2)

In the latter example, A = B = C = 0 and D = [[1., 1., 1.]
[1., 1., 1.]].
Expand All @@ -1205,7 +1205,7 @@ def _convertToStateSpace(sys, **kw):

if isinstance(sys, StateSpace):
if len(kw):
raise TypeError("If sys is a StateSpace, _convertToStateSpace "
raise TypeError("If sys is a StateSpace, _convert_to_statespace "
"cannot take keywords.")

# Already a state space system; just return it
Expand All @@ -1221,7 +1221,7 @@ def _convertToStateSpace(sys, **kw):
from slycot import td04ad
if len(kw):
raise TypeError("If sys is a TransferFunction, "
"_convertToStateSpace cannot take keywords.")
"_convert_to_statespace cannot take keywords.")

# Change the numerator and denominator arrays so that the transfer
# function matrix has a common denominator.
Expand Down Expand Up @@ -1281,11 +1281,8 @@ def _convertToStateSpace(sys, **kw):
try:
D = _ssmatrix(sys)
return StateSpace([], [], [], D)
except Exception as e:
print("Failure to assume argument is matrix-like in"
" _convertToStateSpace, result %s" % e)

raise TypeError("Can't convert given type to StateSpace system.")
except:
raise TypeError("Can't convert given type to StateSpace system.")


# TODO: add discrete time option
Expand Down Expand Up @@ -1662,14 +1659,14 @@ def tf2ss(*args):
from .xferfcn import TransferFunction
if len(args) == 2 or len(args) == 3:
# Assume we were given the num, den
return _convertToStateSpace(TransferFunction(*args))
return _convert_to_statespace(TransferFunction(*args))

elif len(args) == 1:
sys = args[0]
if not isinstance(sys, TransferFunction):
raise TypeError("tf2ss(sys): sys must be a TransferFunction "
"object.")
return _convertToStateSpace(sys)
return _convert_to_statespace(sys)
else:
raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))

Expand Down Expand Up @@ -1769,5 +1766,5 @@ def ssdata(sys):
(A, B, C, D): list of matrices
State space data for the system
"""
ss = _convertToStateSpace(sys)
ss = _convert_to_statespace(sys)
return ss.A, ss.B, ss.C, ss.D
2 changes: 1 addition & 1 deletion control/tests/bdalg_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def test_feedback_args(self, tsys):
ctrl.feedback(*args)

# If second argument is not LTI or convertable, generate an exception
args = (tsys.sys1, np.array([1]))
args = (tsys.sys1, 'hello world')
with pytest.raises(TypeError):
ctrl.feedback(*args)

Expand Down
8 changes: 4 additions & 4 deletions control/tests/frd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import control as ct
from control.statesp import StateSpace
from control.xferfcn import TransferFunction
from control.frdata import FRD, _convertToFRD, FrequencyResponseData
from control.frdata import FRD, _convert_to_FRD, FrequencyResponseData
from control import bdalg, evalfr, freqplot
from control.tests.conftest import slycotonly

Expand Down Expand Up @@ -174,9 +174,9 @@ def testFeedback2(self):

def testAuto(self):
omega = np.logspace(-1, 2, 10)
f1 = _convertToFRD(1, omega)
f2 = _convertToFRD(np.array([[1, 0], [0.1, -1]]), omega)
f2 = _convertToFRD([[1, 0], [0.1, -1]], omega)
f1 = _convert_to_FRD(1, omega)
f2 = _convert_to_FRD(np.array([[1, 0], [0.1, -1]]), omega)
f2 = _convert_to_FRD([[1, 0], [0.1, -1]], omega)
f1, f2 # reference to avoid pyflakes error

def testNyquist(self):
Expand Down
35 changes: 29 additions & 6 deletions control/tests/statesp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@

import numpy as np
import pytest
import operator
from numpy.linalg import solve
from scipy.linalg import block_diag, eigvals

import control as ct
from control.config import defaults
from control.dtime import sample_system
from control.lti import evalfr
from control.statesp import (StateSpace, _convertToStateSpace, drss, rss, ss,
tf2ss, _statesp_defaults)
from control.statesp import (StateSpace, _convert_to_statespace, drss,
rss, ss, tf2ss, _statesp_defaults)
from control.tests.conftest import ismatarrayout, slycotonly
from control.xferfcn import TransferFunction, ss2tf

Expand Down Expand Up @@ -224,7 +226,7 @@ def test_pole(self, sys322):

def test_zero_empty(self):
"""Test to make sure zero() works with no zeros in system."""
sys = _convertToStateSpace(TransferFunction([1], [1, 2, 1]))
sys = _convert_to_statespace(TransferFunction([1], [1, 2, 1]))
np.testing.assert_array_equal(sys.zero(), np.array([]))

@slycotonly
Expand Down Expand Up @@ -456,7 +458,7 @@ def test_append_tf(self):
s = TransferFunction([1, 0], [1])
h = 1 / (s + 1) / (s + 2)
sys1 = StateSpace(A1, B1, C1, D1)
sys2 = _convertToStateSpace(h)
sys2 = _convert_to_statespace(h)
sys3c = sys1.append(sys2)
np.testing.assert_array_almost_equal(sys1.A, sys3c.A[:3, :3])
np.testing.assert_array_almost_equal(sys1.B, sys3c.B[:3, :2])
Expand Down Expand Up @@ -625,10 +627,10 @@ def test_empty(self):
assert 0 == g1.outputs

def test_matrix_to_state_space(self):
"""_convertToStateSpace(matrix) gives ss([],[],[],D)"""
"""_convert_to_statespace(matrix) gives ss([],[],[],D)"""
with pytest.deprecated_call():
D = np.matrix([[1, 2, 3], [4, 5, 6]])
g = _convertToStateSpace(D)
g = _convert_to_statespace(D)

np.testing.assert_array_equal(np.empty((0, 0)), g.A)
np.testing.assert_array_equal(np.empty((0, D.shape[1])), g.B)
Expand Down Expand Up @@ -927,3 +929,24 @@ def test_latex_repr(gmats, ref, repr_type, num_format, editsdefaults):
g = StateSpace(*gmats)
refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
assert g._repr_latex_() == ref[refkey]


@pytest.mark.parametrize(
"op",
[pytest.param(getattr(operator, s), id=s) for s in ('add', 'sub', 'mul')])
@pytest.mark.parametrize(
"tf, arr",
[pytest.param(ct.tf([1], [0.5, 1]), np.array(2.), id="0D scalar"),
pytest.param(ct.tf([1], [0.5, 1]), np.array([2.]), id="1D scalar"),
pytest.param(ct.tf([1], [0.5, 1]), np.array([[2.]]), id="2D scalar")])
def test_xferfcn_ndarray_precedence(op, tf, arr):
# Apply the operator to the transfer function and array
ss = ct.tf2ss(tf)
result = op(ss, arr)
assert isinstance(result, ct.StateSpace)

# Apply the operator to the array and transfer function
ss = ct.tf2ss(tf)
result = op(arr, ss)
assert isinstance(result, ct.StateSpace)

Loading