8000 Merge pull request #498 from murrayrm/array_priority+system_composition · python-control/python-control@2d6c47f · GitHub
[go: up one dir, main page]

Skip to content {"props":{"docsUrl":"https://docs.github.com/get-started/accessibility/keyboard-shortcuts"}}

Commit 2d6c47f

Browse files
authored
Merge pull request #498 from murrayrm/array_priority+system_composition
TransferFunction array priority plus system type conversion checking
2 parents 0eb4396 + e910c22 commit 2d6c47f

File tree

13 files changed

+342
-64
lines changed

13 files changed

+342
-64
lines changed

control/bdalg.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,9 @@ def feedback(sys1, sys2=1, sign=-1):
242242
if isinstance(sys2, tf.TransferFunction):
243243
sys1 = tf._convert_to_transfer_function(sys1)
244244
elif isinstance(sys2, ss.StateSpace):
245-
sys1 = ss._convertToStateSpace(sys1)
245+
sys1 = ss._convert_to_statespace(sys1)
246246
elif isinstance(sys2, frd.FRD):
247-
sys1 = frd._convertToFRD(sys1, sys2.omega)
247+
sys1 = frd._convert_to_FRD(sys1, sys2.omega)
248248
else: # sys2 is a scalar.
249249
sys1 = tf._convert_to_transfer_function(sys1)
250250
sys2 = tf._convert_to_transfer_function(sys2)

control/dtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"""
4848

4949
from .lti import isctime
50-
from .statesp import StateSpace, _convertToStateSpace
50+
from .statesp import StateSpace
5151

5252
__all__ = ['sample_system', 'c2d']
5353

control/frdata.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def __add__(self, other):
197197

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

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

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

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

292292
if (self.inputs > 1 or self.outputs > 1 or
293293
other.inputs > 1 or other.outputs > 1):
@@ -310,7 +310,7 @@ def __rtruediv__(self, other):
310310
return FRD(other / self.fresp, self.omega,
311311
smooth=(self.ifunc is not None))
312312
else:
313-
other = _convertToFRD(other, omega=self.omega)
313+
other = _convert_to_FRD(other, omega=self.omega)
314314

315315
if (self.inputs > 1 or self.outputs > 1 or
316316
other.inputs > 1 or other.outputs > 1):
@@ -450,7 +450,7 @@ def freqresp(self, omega):
450450
def feedback(self, other=1, sign=-1):
451451
"""Feedback interconnection between two FRD objects."""
452452

453-
other = _convertToFRD(other, omega=self.omega)
453+
other = _convert_to_FRD(other, omega=self.omega)
454454

455455
if (self.outputs != other.inputs or self.inputs != other.outputs):
456456
raise ValueError(
@@ -486,7 +486,7 @@ def feedback(self, other=1, sign=-1):
486486
FRD = FrequencyResponseData
487487

488488

489-
def _convertToFRD(sys, omega, inputs=1, outputs=1):
489+
def _convert_to_FRD(sys, omega, inputs=1, outputs=1):
490490
"""Convert a system to frequency response data form (if needed).
491491
492492
If sys is already an frd, and its frequency range matches or
@@ -496,8 +496,8 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
496496
scalar, then the number of inputs and outputs can be specified
497497
manually, as in:
498498
499-
>>> frd = _convertToFRD(3., omega) # Assumes inputs = outputs = 1
500-
>>> frd = _convertToFRD(1., omegs, inputs=3, outputs=2)
499+
>>> frd = _convert_to_FRD(3., omega) # Assumes inputs = outputs = 1
500+
>>> frd = _convert_to_FRD(1., omegs, inputs=3, outputs=2)
501501
502502
In the latter example, sys's matrix transfer function is [[1., 1., 1.]
503503
[1., 1., 1.]].

control/iosys.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def __mul__(sys2, sys1):
220220
raise NotImplemented("Matrix multiplication not yet implemented")
221221

222222
elif not isinstance(sys1, InputOutputSystem):
223-
raise ValueError("Unknown I/O system object ", sys1)
223+
raise TypeError("Unknown I/O system object ", sys1)
224224

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

255255
def __rmul__(sys1, sys2):
256256
"""Pre-multiply an input/output systems by a scalar/matrix"""
257-
if isinstance(sys2, (int, float, np.number)):
257+
if isinstance(sys2, InputOutputSystem):
258+
# Both systems are InputOutputSystems => use __mul__
259+
return InputOutputSystem.__mul__(sys2, sys1)
260+
261+
elif isinstance(sys2, (int, float, np.number)):
258262
# TODO: Scale the output
259263
raise NotImplemented("Scalar multiplication not yet implemented")
260264

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

265-
elif not isinstance(sys2, InputOutputSystem):
266-
raise ValueError("Unknown I/O system object ", sys1)
269+
elif isinstance(sys2, StateSpace):
270+
# TODO: Should eventuall preserve LinearIOSystem structure
271+
return StateSpace.__mul__(sys2, sys1)
267272

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

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

283287
elif not isinstance(sys2, InputOutputSystem):
284-
raise ValueError("Unknown I/O system object ", sys2)
288+
raise TypeError("Unknown I/O system object ", sys2)
285289

286290
# Make sure number of input and outputs match
287291
if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs:

control/statesp.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
# Python 3 compatibility (needs to go here)
1212
from __future__ import print_function
13-
from __future__ import division # for _convertToStateSpace
13+
from __future__ import division # for _convert_to_statespace
1414

1515
"""Copyright (c) 2010 by California Institute of Technology
1616
All rights reserved.
@@ -527,7 +527,7 @@ def __add__(self, other):
527527
D = self.D + other
528528
dt = self.dt
529529
else:
530-
other = _convertToStateSpace(other)
530+
other = _convert_to_statespace(other)
531531

532532
# Check to make sure the dimensions are OK
533533
if ((self.inputs != other.inputs) or
@@ -577,7 +577,7 @@ def __mul__(self, other):
577577
D = self.D * other
578578
dt = self.dt
579579
else:
580-
other = _convertToStateSpace(other)
580+
other = _convert_to_statespace(other)
581581

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

615615
# is lti, and convertible?
616616
if isinstance(other, LTI):
617-
return _convertToStateSpace(other) * self
617+
return _convert_to_statespace(other) * self
618618

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

842-
other = _convertToStateSpace(other)
842+
other = _convert_to_statespace(other)
843843

844844
# Check to make sure the dimensions are OK
845845
if (self.inputs != other.outputs) or (self.outputs != other.inputs):
@@ -907,7 +907,7 @@ def lft(self, other, nu=-1, ny=-1):
907907
Dimension of (plant) control input.
908908
909909
"""
910-
other = _convertToStateSpace(other)
910+
other = _convert_to_statespace(other)
911911
# maximal values for nu, ny
912912
if ny == -1:
913913
ny = min(other.inputs, self.outputs)
@@ -1061,7 +1061,7 @@ def append(self, other):
10611061
The second model is converted to state-space if necessary, inputs and
10621062
outputs are appended and their order is preserved"""
10631063
if not isinstance(other, StateSpace):
1064-
other = _convertToStateSpace(other)
1064+
other = _convert_to_statespace(other)
10651065

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

@@ -1186,16 +1186,16 @@ def is_static_gain(self):
11861186

11871187

11881188
# TODO: add discrete time check
1189-
def _convertToStateSpace(sys, **kw):
1189+
def _convert_to_statespace(sys, **kw):
11901190
"""Convert a system to state space form (if needed).
11911191
11921192
If sys is already a state space, then it is returned. If sys is a
11931193
transfer function object, then it is converted to a state space and
11941194
returned. If sys is a scalar, then the number of inputs and outputs can
11951195
be specified manually, as in:
11961196
1197-
>>> sys = _convertToStateSpace(3.) # Assumes inputs = outputs = 1
1198-
>>> sys = _convertToStateSpace(1., inputs=3, outputs=2)
1197+
>>> sys = _convert_to_statespace(3.) # Assumes inputs = outputs = 1
1198+
>>> sys = _convert_to_statespace(1., inputs=3, outputs=2)
11991199
12001200
In the latter example, A = B = C = 0 and D = [[1., 1., 1.]
12011201
[1., 1., 1.]].
@@ -1205,7 +1205,7 @@ def _convertToStateSpace(sys, **kw):
12051205

12061206
if isinstance(sys, StateSpace):
12071207
if len(kw):
1208-
raise TypeError("If sys is a StateSpace, _convertToStateSpace "
1208+
raise TypeError("If sys is a StateSpace, _convert_to_statespace "
12091209
"cannot take keywords.")
12101210

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

12261226
# Change the numerator and denominator arrays so that the transfer
12271227
# function matrix has a common denominator.
@@ -1281,11 +1281,8 @@ def _convertToStateSpace(sys, **kw):
12811281
try:
12821282
D = _ssmatrix(sys)
12831283
return StateSpace([], [], [], D)
1284-
except Exception as e:
1285-
print("Failure to assume argument is matrix-like in"
1286-
" _convertToStateSpace, result %s" % e)
1287-
1288-
raise TypeError("Can't convert given type to StateSpace system.")
1284+
except:
1285+
raise TypeError("Can't convert given type to StateSpace system.")
12891286

12901287

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

16671664
elif len(args) == 1:
16681665
sys = args[0]
16691666
if not isinstance(sys, TransferFunction):
16701667
raise TypeError("tf2ss(sys): sys must be a TransferFunction "
16711668
"object.")
1672-
return _convertToStateSpace(sys)
1669+
return _convert_to_statespace(sys)
16731670
else:
16741671
raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))
16751672

@@ -1769,5 +1766,5 @@ def ssdata(sys):
17691766
(A, B, C, D): list of matrices
17701767
State space data for the system
17711768
"""
1772-
ss = _convertToStateSpace(sys)
1769+
ss = _convert_to_statespace(sys)
17731770
return ss.A, ss.B, ss.C, ss.D

control/tests/bdalg_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def test_feedback_args(self, tsys):
255255
ctrl.feedback(*args)
256256

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

control/tests/frd_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import control as ct
1313
from control.statesp import StateSpace
1414
from control.xferfcn import TransferFunction
15-
from control.frdata import FRD, _convertToFRD, FrequencyResponseData
15+
from control.frdata import FRD, _convert_to_FRD, FrequencyResponseData
1616
from control import bdalg, evalfr, freqplot
1717
from control.tests.conftest import slycotonly
1818

@@ -174,9 +174,9 @@ def testFeedback2(self):
174174

175175
def testAuto(self):
176176
omega = np.logspace(-1, 2, 10)
177-
f1 = _convertToFRD(1, omega)
178-
f2 = _convertToFRD(np.array([[1, 0], [0.1, -1]]), omega)
179-
f2 = _convertToFRD([[1, 0], [0.1, -1]], omega)
177+
f1 = _convert_to_FRD(1, omega)
178+
f2 = _convert_to_FRD(np.array([[1, 0], [0.1, -1]]), omega)
179+
f2 = _convert_to_FRD([[1, 0], [0.1, -1]], omega)
180180
f1, f2 # reference to avoid pyflakes error
181181

182182
def testNyquist(self):

control/tests/statesp_test.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99

1010
import numpy as np
1111
import pytest
12+
import operator
1213
from numpy.linalg import solve
1314
from scipy.linalg import block_diag, eigvals
1415

16+
import control as ct
1517
from control.config import defaults
1618
from control.dtime import sample_system
1719
from control.lti import evalfr
18-
from control.statesp import (StateSpace, _convertToStateSpace, drss, rss, ss,
19-
tf2ss, _statesp_defaults)
20+
from control.statesp import (StateSpace, _convert_to_statespace, drss,
21+
rss, ss, tf2ss, _statesp_defaults)
2022
from control.tests.conftest import ismatarrayout, slycotonly
2123
from control.xferfcn import TransferFunction, ss2tf
2224

@@ -224,7 +226,7 @@ def test_pole(self, sys322):
224226

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

230232
@slycotonly
@@ -456,7 +458,7 @@ def test_append_tf(self):
456458
s = TransferFunction([1, 0], [1])
457459
h = 1 / (s + 1) / (s + 2)
458460
sys1 = StateSpace(A1, B1, C1, D1)
459-
sys2 = _convertToStateSpace(h)
461+
sys2 = _convert_to_statespace(h)
460462
sys3c = sys1.append(sys2)
461463
np.testing.assert_array_almost_equal(sys1.A, sys3c.A[:3, :3])
462464
np.testing.assert_array_almost_equal(sys1.B, sys3c.B[:3, :2])
@@ -625,10 +627,10 @@ def test_empty(self):
625627
assert 0 == g1.outputs
626628

627629
def test_matrix_to_state_space(self):
628-
"""_convertToStateSpace(matrix) gives ss([],[],[],D)"""
630+
"""_convert_to_statespace(matrix) gives ss([],[],[],D)"""
629631
with pytest.deprecated_call():
630632
D = np.matrix([[1, 2, 3], [4, 5, 6]])
631-
g = _convertToStateSpace(D)
633+
g = _convert_to_statespace(D)
632634

633635
np.testing.assert_array_equal(np.empty((0, 0)), g.A)
634636
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):
927929
g = StateSpace(*gmats)
928930
refkey = "{}_{}".format(refkey_n[num_format], refkey_r[repr_type])
929931
assert g._repr_latex_() == ref[refkey]
932+
933+
934+
@pytest.mark.parametrize(
935+
"op",
936+
[pytest.param(getattr(operator, s), id=s) for s in ('add', 'sub', 'mul')])
937+
@pytest.mark.parametrize(
938+
"tf, arr",
939+
[pytest.param(ct.tf([1], [0.5, 1]), np.array(2.), id="0D scalar"),
940+
pytest.param(ct.tf([1], [0.5, 1]), np.array([2.]), id="1D scalar"),
941+
pytest.param(ct.tf([1], [0.5, 1]), np.array([[2.]]), id="2D scalar")])
942+
def test_xferfcn_ndarray_precedence(op, tf, arr):
943+
# Apply the operator to the transfer function and array
944+
ss = ct.tf2ss(tf)
945+
result = op(ss, arr)
946+
assert isinstance(result, ct.StateSpace)
947+
948+
# Apply the operator to the array and transfer function
949+
ss = ct.tf2ss(tf)
950+
result = op(arr, ss)
951+
assert isinstance(result, ct.StateSpace)
952+

0 commit comments

Comments
 (0)
0