diff --git a/control/bdalg.py b/control/bdalg.py index f88e8e813..e00dcfa3c 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -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) diff --git a/control/dtime.py b/control/dtime.py index 725bcde47..8c0fe53e9 100644 --- a/control/dtime.py +++ b/control/dtime.py @@ -47,7 +47,7 @@ """ from .lti import isctime -from .statesp import StateSpace, _convertToStateSpace +from .statesp import StateSpace __all__ = ['sample_system', 'c2d'] diff --git a/control/frdata.py b/control/frdata.py index 22dbb298f..8f148a3fa 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -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: @@ -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: @@ -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: @@ -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): @@ -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): @@ -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( @@ -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 @@ -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.]]. diff --git a/control/iosys.py b/control/iosys.py index 94b8234c6..913e8d471 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -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: @@ -254,7 +254,11 @@ 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") @@ -262,12 +266,12 @@ def __rmul__(sys1, sys2): # 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)""" @@ -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: diff --git a/control/statesp.py b/control/statesp.py index ffd229108..ff4c73c4e 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -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. @@ -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 @@ -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: @@ -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: @@ -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): @@ -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) @@ -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) @@ -1186,7 +1186,7 @@ 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 @@ -1194,8 +1194,8 @@ def _convertToStateSpace(sys, **kw): 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.]]. @@ -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 @@ -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. @@ -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 @@ -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)) @@ -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 diff --git a/control/tests/bdalg_test.py b/control/tests/bdalg_test.py index fc5f78f91..433a584cc 100644 --- a/control/tests/bdalg_test.py +++ b/control/tests/bdalg_test.py @@ -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) diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py index f8ee3eb20..c63a4c02b 100644 --- a/control/tests/frd_test.py +++ b/control/tests/frd_test.py @@ -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 @@ -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): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 48f27a3b5..8a91da68b 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -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 @@ -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 @@ -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]) @@ -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) @@ -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) + diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py new file mode 100644 index 000000000..3f51c2bbc --- /dev/null +++ b/control/tests/type_conversion_test.py @@ -0,0 +1,187 @@ +# type_conversion_test.py - test type conversions +# RMM, 3 Jan 2021 +# +# This set of tests looks at how various classes are converted when using +# algebraic operations. See GitHub issue #459 for some discussion on what the +# desired combinations should be. + +import control as ct +import numpy as np +import operator +import pytest + +@pytest.fixture() +def sys_dict(): + sdict = {} + sdict['ss'] = ct.ss([[-1]], [[1]], [[1]], [[0]]) + sdict['tf'] = ct.tf([1],[0.5, 1]) + sdict['tfx'] = ct.tf([1, 1],[1]) # non-proper transfer function + sdict['frd'] = ct.frd([10+0j, 9 + 1j, 8 + 2j], [1,2,3]) + sdict['lio'] = ct.LinearIOSystem(ct.ss([[-1]], [[5]], [[5]], [[0]])) + sdict['ios'] = ct.NonlinearIOSystem( + sdict['lio']._rhs, sdict['lio']._out, 1, 1, 1) + sdict['arr'] = np.array([[2.0]]) + sdict['flt'] = 3. + return sdict + +type_dict = { + 'ss': ct.StateSpace, 'tf': ct.TransferFunction, + 'frd': ct.FrequencyResponseData, 'lio': ct.LinearICSystem, + 'ios': ct.InterconnectedSystem, 'arr': np.ndarray, 'flt': float} + +# +# Current table of expected conversions +# +# This table describes all of the conversions that are supposed to +# happen for various system combinations. This is written out this way +# to make it easy to read, but this is converted below into a list of +# specific tests that can be iterated over. +# +# Items marked as 'E' should generate an exception. +# +# Items starting with 'x' currently generate an expected exception but +# should eventually generate a useful result (when everything is +# implemented properly). +# +# Note 1: some of the entries below are currently converted to to lower level +# types than needed. In particular, LinearIOSystems should combine with +# StateSpace and TransferFunctions in a way that preserves I/O system +# structure when possible. +# +# Note 2: eventually the operator entry for this table can be pulled out and +# tested as a separate parameterized variable (since all operators should +# return consistent values). +# +# Note 3: this table documents the current state, but not actually the desired +# state. See bottom of the file for the (eventual) desired behavior. +# + +rtype_list = ['ss', 'tf', 'frd', 'lio', 'ios', 'arr', 'flt'] +conversion_table = [ + # op left ss tf frd lio ios arr flt + ('add', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('add', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), + ('add', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), + ('add', 'lio', ['xio', 'xio', 'xrd', 'lio', 'ios', 'xio', 'xio']), + ('add', 'ios', ['xos', 'xos', 'E', 'ios', 'ios', 'xos', 'xos']), + ('add', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), + ('add', 'flt', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'flt']), + + # op left ss tf frd lio ios arr flt + ('sub', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('sub', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), + ('sub', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'xrd']), + ('sub', 'lio', ['xio', 'xio', 'xrd', 'lio', 'ios', 'xio', 'xio']), + ('sub', 'ios', ['xos', 'xio', 'E', 'ios', 'xos' 'xos', 'xos']), + ('sub', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), + ('sub', 'flt', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'flt']), + + # op left ss tf frd lio ios arr flt + ('mul', 'ss', ['ss', 'ss', 'xrd', 'ss', 'xos', 'ss', 'ss' ]), + ('mul', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), + ('mul', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'frd']), + ('mul', 'lio', ['xio', 'xio', 'xrd', 'lio', 'ios', 'xio', 'xio']), + ('mul', 'ios', ['xos', 'xos', 'E', 'ios', 'ios', 'xos', 'xos']), + ('mul', 'arr', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), + ('mul', 'flt', ['ss', 'tf', 'frd', 'xio', 'xos', 'arr', 'flt']), + + # op left ss tf frd lio ios arr flt + ('truediv', 'ss', ['xs', 'tf', 'xrd', 'xio', 'xos', 'xs', 'xs' ]), + ('truediv', 'tf', ['tf', 'tf', 'xrd', 'tf', 'xos', 'tf', 'tf' ]), + ('truediv', 'frd', ['xrd', 'xrd', 'frd', 'xrd', 'E', 'xrd', 'frd']), + ('truediv', 'lio', ['xio', 'tf', 'xrd', 'xio', 'xio', 'xio', 'xio']), + ('truediv', 'ios', ['xos', 'xos', 'E', 'xos', 'xos' 'xos', 'xos']), + ('truediv', 'arr', ['xs', 'tf', 'xrd', 'xio', 'xos', 'arr', 'arr']), + ('truediv', 'flt', ['xs', 'tf', 'frd', 'xio', 'xos', 'arr', 'flt'])] + +# Now create list of the tests we actually want to run +test_matrix = [] +for i, (opname, ltype, expected_list) in enumerate(conversion_table): + for rtype, expected in zip(rtype_list, expected_list): + # Add this to the list of tests to run + test_matrix.append([opname, ltype, rtype, expected]) + +@pytest.mark.parametrize("opname, ltype, rtype, expected", test_matrix) +def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): + op = getattr(operator, opname) + leftsys = sys_dict[ltype] + rightsys = sys_dict[rtype] + + # Get rid of warnings for InputOutputSystem objects by making a copy + if isinstance(leftsys, ct.InputOutputSystem) and leftsys == rightsys: + rightsys = leftsys.copy() + + # Make sure we get the right result + if expected == 'E' or expected[0] == 'x': + # Exception expected + with pytest.raises(TypeError): + op(leftsys, rightsys) + else: + # Operation should work and return the given type + result = op(leftsys, rightsys) + + # Print out what we are testing in case something goes wrong + assert isinstance(result, type_dict[expected]) + +# +# Updated table that describes desired outputs for all operators +# +# General rules (subject to change) +# +# * For LTI/LTI, keep the type of the left operand whenever possible. This +# prioritizes the first operand, but we need to watch out for non-proper +# transfer functions (in which case TransferFunction should be returned) +# +# * For FRD/LTI, convert LTI to FRD by evaluating the LTI transfer function +# at the FRD frequencies (can't got the other way since we can't convert +# an FRD object to state space/transfer function). +# +# * For IOS/LTI, convert to IOS. In the case of a linear I/O system (LIO), +# this will preserve the linear structure since the LTI system will +# be converted to state space. +# +# * When combining state space or transfer with linear I/O systems, the +# * output should be of type Linear IO system, since that maintains the +# * underlying state space attributes. +# +# Note: tfx = non-proper transfer function, order(num) > order(den) +# + +type_list = ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] +conversion_table = [ + # L \ R ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] + ('ss', ['ss', 'ss', 'tf' 'frd', 'lio', 'ios', 'ss', 'ss' ]), + ('tf', ['tf', 'tf', 'tf' 'frd', 'lio', 'ios', 'tf', 'tf' ]), + ('tfx', ['tf', 'tf', 'tf', 'frd', 'E', 'E', 'tf', 'tf' ]), + ('frd', ['frd', 'frd', 'frd', 'frd', 'E', 'E', 'frd', 'frd']), + ('lio', ['lio', 'lio', 'E', 'E', 'lio', 'ios', 'lio', 'lio']), + ('ios', ['ios', 'ios', 'E', 'E', 'ios', 'ios', 'ios', 'ios']), + ('arr', ['ss', 'tf', 'tf' 'frd', 'lio', 'ios', 'arr', 'arr']), + ('flt', ['ss', 'tf', 'tf' 'frd', 'lio', 'ios', 'arr', 'flt'])] + +@pytest.mark.skip(reason="future test; conversions not yet fully implemented") +# @pytest.mark.parametrize("opname", ['add', 'sub', 'mul', 'truediv']) +# @pytest.mark.parametrize("ltype", type_list) +# @pytest.mark.parametrize("rtype", type_list) +def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict): + op = getattr(operator, opname) + leftsys = sys_dict[ltype] + rightsys = sys_dict[rtype] + expected = \ + conversion_table[type_list.index(ltype)][1][type_list.index(rtype)] + + # Get rid of warnings for InputOutputSystem objects by making a copy + if isinstance(leftsys, ct.InputOutputSystem) and leftsys == rightsys: + rightsys = leftsys.copy() + + # Make sure we get the right result + if expected == 'E' or expected[0] == 'x': + # Exception expected + with pytest.raises(TypeError): + op(leftsys, rightsys) + else: + # Operation should work and return the given type + result = op(leftsys, rightsys) + + # Print out what we are testing in case something goes wrong + assert isinstance(result, type_dict[expected]) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index eb8755f82..4fc88c42e 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -5,8 +5,10 @@ import numpy as np import pytest +import operator -from control.statesp import StateSpace, _convertToStateSpace, rss +import control as ct +from control.statesp import StateSpace, _convert_to_statespace, rss from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ ss2tf from control.lti import evalfr @@ -709,7 +711,7 @@ def test_state_space_conversion_mimo(self): h = (b0 + b1*s + b2*s**2)/(a0 + a1*s + a2*s**2 + a3*s**3) H = TransferFunction([[h.num[0][0]], [(h*s).num[0][0]]], [[h.den[0][0]], [h.den[0][0]]]) - sys = _convertToStateSpace(H) + sys = _convert_to_statespace(H) H2 = _convert_to_transfer_function(sys) np.testing.assert_array_almost_equal(H.num[0][0], H2.num[0][0]) np.testing.assert_array_almost_equal(H.den[0][0], H2.den[0][0]) @@ -1022,3 +1024,20 @@ def test_returnScipySignalLTI_error(self, mimotf): mimotf.returnScipySignalLTI() with pytest.raises(ValueError): mimotf.returnScipySignalLTI(strict=True) + +@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 + result = op(tf, arr) + assert isinstance(result, ct.TransferFunction) + + # Apply the operator to the array and transfer function + result = op(arr, tf) + assert isinstance(result, ct.TransferFunction) diff --git a/control/timeresp.py b/control/timeresp.py index f4f293bdf..a5cc245bf 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -79,7 +79,7 @@ atleast_1d) import warnings from .lti import LTI # base class of StateSpace, TransferFunction -from .statesp import _convertToStateSpace, _mimo2simo, _mimo2siso, ssdata +from .statesp import _convert_to_statespace, _mimo2simo, _mimo2siso, ssdata from .lti import isdtime, isctime __all__ = ['forced_response', 'step_response', 'step_info', 'initial_response', @@ -271,7 +271,7 @@ def forced_response(sys, T=None, U=0., X0=0., transpose=False, if not isinstance(sys, LTI): raise TypeError('Parameter ``sys``: must be a ``LTI`` object. ' '(For example ``StateSpace`` or ``TransferFunction``)') - sys = _convertToStateSpace(sys) + sys = _convert_to_statespace(sys) A, B, C, D = np.asarray(sys.A), np.asarray(sys.B), np.asarray(sys.C), \ np.asarray(sys.D) # d_type = A.dtype @@ -436,7 +436,7 @@ def _get_ss_simo(sys, input=None, output=None): If input is not specified, select first input and issue warning """ - sys_ss = _convertToStateSpace(sys) + sys_ss = _convert_to_statespace(sys) if sys_ss.issiso(): return sys_ss warn = False @@ -891,7 +891,7 @@ def _ideal_tfinal_and_dt(sys, is_step=True): dt = sys.dt if isdtime(sys, strict=True) else default_dt elif isdtime(sys, strict=True): dt = sys.dt - A = _convertToStateSpace(sys).A + A = _convert_to_statespace(sys).A tfinal = default_tfinal p = eigvals(A) # Array Masks @@ -931,7 +931,7 @@ def _ideal_tfinal_and_dt(sys, is_step=True): if p_int.size > 0: tfinal = tfinal * 5 else: # cont time - sys_ss = _convertToStateSpace(sys) + sys_ss = _convert_to_statespace(sys) # Improve conditioning via balancing and zeroing tiny entries # See for [[1,2,0], [9,1,0.01], [1,2,10*np.pi]] before/after balance b, (sca, perm) = matrix_balance(sys_ss.A, separate=True) diff --git a/control/xferfcn.py b/control/xferfcn.py index fba674efe..0ff21a42a 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -114,6 +114,9 @@ class TransferFunction(LTI): >>> G = (s + 1)/(s**2 + 2*s + 1) """ + # Give TransferFunction._rmul_() priority for ndarray * TransferFunction + __array_priority__ = 11 # override ndarray and matrix types + def __init__(self, *args, **kwargs): """TransferFunction(num, den[, dt]) @@ -1282,16 +1285,13 @@ def _convert_to_transfer_function(sys, **kw): # If this is array-like, try to create a constant feedthrough try: - D = array(sys) + D = array(sys, ndmin=2) outputs, inputs = D.shape num = [[[D[i, j]] for j in range(inputs)] for i in range(outputs)] den = [[[1] for j in range(inputs)] for i in range(outputs)] return TransferFunction(num, den) - except Exception as e: - print("Failure to assume argument is matrix-like in" - " _convertToTransferFunction, result %s" % e) - - raise TypeError("Can't convert given type to TransferFunction system.") + except: + raise TypeError("Can't convert given type to TransferFunction system.") def tf(*args, **kwargs): diff --git a/doc/index.rst b/doc/index.rst index b6c44d387..cfd4fbd1a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -49,11 +49,59 @@ or to test the installed package:: .. _pytest: https://docs.pytest.org/ -Your contributions are welcome! Simply fork the `GitHub repository `_ and send a +.. rubric:: Contributing + +Your contributions are welcome! Simply fork the `GitHub repository `_ and send a `pull request`_. .. _pull request: https://github.com/python-control/python-control/pulls +The following details suggested steps for making your own contributions to the project using GitHub + +1. Fork on GitHub: login/create an account and click Fork button at the top right corner of https://github.com/python-control/python-control/. + +2. Clone to computer (Replace [you] with your Github username):: + + git clone https://github.com/[you]/python-control.git + cd python_control + +3. Set up remote upstream:: + + git remote add upstream https://github.com/python-control/python-control.git + +4. Start working on a new issue or feature by first creating a new branch with a descriptive name:: + + git checkout -b + +5. Write great code. Suggestion: write the tests you would like your code to satisfy before writing the code itself. This is known as test-driven development. + +6. Run tests and fix as necessary until everything passes:: + + pytest -v + + (for documentation, run ``make html`` in ``doc`` directory) + +7. Commit changes:: + + git add + git commit -m "commit message" + +8. Update & sync your local code to the upstream version on Github before submitting (especially if it has been awhile):: + + git checkout master; git fetch --all; git merge upstream/master; git push + + and then bring those changes into your branch:: + + git checkout ; git rebase master + +9. Push your branch to GitHub:: + + git push origin + +10. Issue pull request to submit your code modifications to Github by going to your fork on Github, clicking Pull Request, and entering a description. +11. Repeat steps 5--9 until feature is complete + + .. rubric:: Links - Issue tracker: https://github.com/python-control/python-control/issues pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy