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' ]), 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