From bb8132e2a2e9c594e9e04e0521b391a16cb9e2ea Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 21 Dec 2020 14:17:58 -0800 Subject: [PATCH 1/9] dev instructions --- doc/index.rst | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) 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 From 8793d780a0ed83cc7b97e94de88a1aa72b7463f9 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 13:20:40 -0800 Subject: [PATCH 2/9] set array_priority=11 for TransferFunction, to match StateSpace --- control/tests/xferfcn_test.py | 19 +++++++++++++++++++ control/xferfcn.py | 3 +++ 2 files changed, 22 insertions(+) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index eb8755f82..50867e887 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -5,7 +5,9 @@ import numpy as np import pytest +import operator +import control as ct from control.statesp import StateSpace, _convertToStateSpace, rss from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ ss2tf @@ -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/xferfcn.py b/control/xferfcn.py index fba674efe..2cf5a5001 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]) From bd77d71cf7f98bae5851f5be6935fdcde6460a9d Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 13:24:06 -0800 Subject: [PATCH 3/9] update _convert_to_transfer_function to allow 0D and 1D arrays --- control/tests/bdalg_test.py | 2 +- control/tests/xferfcn_test.py | 4 ++-- control/xferfcn.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/xferfcn_test.py b/control/tests/xferfcn_test.py index 50867e887..5de7fffca 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -1030,8 +1030,8 @@ def test_returnScipySignalLTI_error(self, mimotf): [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="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 diff --git a/control/xferfcn.py b/control/xferfcn.py index 2cf5a5001..9a0e8ee6d 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1285,7 +1285,7 @@ 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)] From 4b7bf8a7e9ca26b198ae6a99818fe165c6b2639f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 13:40:29 -0800 Subject: [PATCH 4/9] rename _convertToX to _convert_to_x + statesp/ndarray unit tests --- control/bdalg.py | 4 ++-- control/dtime.py | 2 +- control/frdata.py | 18 +++++++++--------- control/statesp.py | 32 ++++++++++++++++---------------- control/tests/frd_test.py | 8 ++++---- control/tests/statesp_test.py | 35 +++++++++++++++++++++++++++++------ control/tests/xferfcn_test.py | 4 ++-- control/timeresp.py | 10 +++++----- 8 files changed, 68 insertions(+), 45 deletions(-) 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/statesp.py b/control/statesp.py index ffd229108..35b95a80d 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. @@ -1283,7 +1283,7 @@ def _convertToStateSpace(sys, **kw): return StateSpace([], [], [], D) except Exception as e: print("Failure to assume argument is matrix-like in" - " _convertToStateSpace, result %s" % e) + " _convert_to_statespace, result %s" % e) raise TypeError("Can't convert given type to StateSpace system.") @@ -1662,14 +1662,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 +1769,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/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/xferfcn_test.py b/control/tests/xferfcn_test.py index 5de7fffca..4fc88c42e 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -8,7 +8,7 @@ import operator import control as ct -from control.statesp import StateSpace, _convertToStateSpace, rss +from control.statesp import StateSpace, _convert_to_statespace, rss from control.xferfcn import TransferFunction, _convert_to_transfer_function, \ ss2tf from control.lti import evalfr @@ -711,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]) 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) From 94b62098d1f16d0e4930962d1d192fa34cec4bbc Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 16:41:42 -0800 Subject: [PATCH 5/9] fix converstion exceptions to be TypeError --- control/iosys.py | 6 +++--- control/statesp.py | 7 ++----- control/xferfcn.py | 7 ++----- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 94b8234c6..efce73e49 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: @@ -263,7 +263,7 @@ def __rmul__(sys1, sys2): raise NotImplemented("Matrix multiplication not yet implemented") elif not isinstance(sys2, InputOutputSystem): - raise ValueError("Unknown I/O system object ", sys1) + raise TypeError("Unknown I/O system object ", sys1) else: # Both systems are InputOutputSystems => use __mul__ @@ -281,7 +281,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 35b95a80d..ff4c73c4e 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1281,11 +1281,8 @@ def _convert_to_statespace(sys, **kw): try: D = _ssmatrix(sys) return StateSpace([], [], [], D) - except Exception as e: - print("Failure to assume argument is matrix-like in" - " _convert_to_statespace, 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 diff --git a/control/xferfcn.py b/control/xferfcn.py index 9a0e8ee6d..0ff21a42a 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -1290,11 +1290,8 @@ def _convert_to_transfer_function(sys, **kw): 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): From 67a05617a26e70c3862b568eba84c042b15d50b0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 21:19:25 -0800 Subject: [PATCH 6/9] add unit tests for checking type converstions --- control/tests/type_conversion_test.py | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 control/tests/type_conversion_test.py diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py new file mode 100644 index 000000000..d574263a3 --- /dev/null +++ b/control/tests/type_conversion_test.py @@ -0,0 +1,119 @@ +# 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['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} + +# +# 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). + +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', 'xio', '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_xferfcn_ndarray_precedence(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]) From 4a24c84522d6144303d71db3a6bc263837e10410 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 3 Jan 2021 21:38:17 -0800 Subject: [PATCH 7/9] update LinearIOSystem.__rmul__ for Python2/Python3 consistency --- control/iosys.py | 14 +++++++++----- control/tests/type_conversion_test.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index efce73e49..913e8d471 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -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 TypeError("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)""" diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index d574263a3..44c6618d8 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -72,7 +72,7 @@ def sys_dict(): ('sub', 'flt', ['ss', 'tf', 'xrd', 'xio', 'xos', 'arr', 'flt']), # op left ss tf frd lio ios arr flt - ('mul', 'ss', ['ss', 'ss', 'xrd', 'xio', 'xos', 'ss', 'ss' ]), + ('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']), From f0593fab671a53a7c083cf403ac8137bf453a1eb Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 4 Jan 2021 22:38:40 -0800 Subject: [PATCH 8/9] add (skipped) function for desired binary operator conversions --- control/tests/type_conversion_test.py | 72 ++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index 44c6618d8..72a02e00f 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -15,6 +15,7 @@ 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( @@ -29,7 +30,7 @@ def sys_dict(): 'ios': ct.InterconnectedSystem, 'arr': np.ndarray, 'flt': float} # -# Table of expected conversions +# 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 @@ -50,6 +51,10 @@ def sys_dict(): # 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 = [ @@ -97,7 +102,7 @@ def sys_dict(): test_matrix.append([opname, ltype, rtype, expected]) @pytest.mark.parametrize("opname, ltype, rtype, expected", test_matrix) -def test_xferfcn_ndarray_precedence(opname, ltype, rtype, expected, sys_dict): +def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): op = getattr(operator, opname) leftsys = sys_dict[ltype] rightsys = sys_dict[rtype] @@ -117,3 +122,66 @@ def test_xferfcn_ndarray_precedence(opname, ltype, rtype, expected, sys_dict): # 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]) From e910c221ce2fd13f5a9d70eed9424de2a6a84073 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 5 Jan 2021 07:37:33 -0800 Subject: [PATCH 9/9] Update control/tests/type_conversion_test.py Co-authored-by: Ben Greiner --- control/tests/type_conversion_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control/tests/type_conversion_test.py b/control/tests/type_conversion_test.py index 72a02e00f..3f51c2bbc 100644 --- a/control/tests/type_conversion_test.py +++ b/control/tests/type_conversion_test.py @@ -149,7 +149,7 @@ def test_operator_type_conversion(opname, ltype, rtype, expected, sys_dict): type_list = ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] conversion_table = [ - # L / R ['ss', 'tf', 'tfx', 'frd', 'lio', 'ios', 'arr', 'flt'] + # 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' ]),