Skip to content

Commit 28204cc

Browse files
committed
2 parents 05d6bc5 + bb65f31 commit 28204cc

31 files changed

Lines changed: 3411 additions & 290 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ __conda_*.txt
99
record.txt
1010
build.log
1111
*.egg-info/
12+
.eggs/
1213
.coverage
1314
doc/_build
1415
doc/generated
@@ -18,3 +19,7 @@ examples/.ipynb_checkpoints/
1819
.project
1920
Untitled*.ipynb
2021
*.idea/
22+
23+
# Files created by or for emacs (RMM, 29 Dec 2017)
24+
*~
25+
TAGS

.travis.yml

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,31 @@ cache:
99
- $HOME/.local
1010

1111
python:
12-
- "2.7"
13-
- "3.3"
14-
- "3.4"
12+
- "3.6"
1513
- "3.5"
14+
- "2.7"
15+
16+
# Test against multiple version of SciPy, with and without slycot
17+
#
18+
# Because there were significant changes in SciPy between v0 and v1, we
19+
# test against both of these using the Travis CI environment capability
20+
#
21+
# We also want to test with and without slycot
22+
env:
23+
- SCIPY=scipy SLYCOT=slycot # default, with slycot
24+
- SCIPY=scipy SLYCOT= # default, w/out slycot
25+
- SCIPY="scipy==0.19.1" SLYCOT= # legacy support, w/out slycot
1626

1727
# install required system libraries
1828
before_install:
29+
# Install gfortran for testing slycot; use apt-get instead of conda in
30+
# order to include the proper CXXABI dependency (updated in GCC 4.9)
31+
# Also need to include liblapack here, to make sure paths are right
32+
- if [[ "$SLYCOT" != "" ]]; then
33+
sudo apt-get update -qq;
34+
sudo apt-get install gfortran liblapack-dev;
35+
fi
36+
# Install display manager to allow testing of plotting functions
1937
- export DISPLAY=:99.0
2038
- sh -e /etc/init.d/xvfb start
2139
# use miniconda to install numpy/scipy, to avoid lengthy build from source
@@ -29,27 +47,30 @@ before_install:
2947
- hash -r
3048
- conda config --set always_yes yes --set changeps1 no
3149
- conda update -q conda
32-
# conda-build must be installed in the conda root environment
33-
- conda install conda-build
3450
- conda config --add channels python-control
3551
- conda info -a
3652
- conda create -q -n test-environment python="$TRAVIS_PYTHON_VERSION" pip coverage
3753
- source activate test-environment
38-
# coveralls not in conda repos
54+
# Make sure to look in the right place for python libraries (for slycot)
55+
- export LIBRARY_PATH="$HOME/miniconda/envs/test-environment/lib"
56+
# coveralls not in conda repos => install via pip instead
3957
- pip install coveralls
4058

4159
# Install packages
4260
install:
43-
- conda build --python "$TRAVIS_PYTHON_VERSION" conda-recipe
44-
- conda install control --use-local
61+
# Install packages needed by python-control
62+
- conda install $SCIPY matplotlib
63+
# Build slycot from source
64+
# For python 3, need to provide pointer to python library
65+
#! git clone https://github.com/repagh/Slycot.git slycot;
66+
- if [[ "$SLYCOT" != "" ]]; then
67+
git clone https://github.com/python-control/Slycot.git slycot;
68+
cd slycot; python setup.py install; cd ..;
69+
fi
4570

4671
# command to run tests
4772
script:
48-
# Before installing Slycot
49-
- python setup.py test
50-
51-
# Now, get and use Slycot
52-
- conda install slycot
73+
- 'if [ $SLYCOT != "" ]; then python -c "import slycot"; fi'
5374
- coverage run setup.py test
5475

5576
after_success:

conda-recipe/meta.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package:
22
name: control
3+
version: {{ GIT_DESCRIBE_TAG }}
4+
5+
source:
6+
git_url: ../
37

48
build:
9+
number: {{ GIT_DESCRIBE_NUMBER }}
510
script:
611
- cd $RECIPE_DIR/..
712
- $PYTHON make_version.py

control/bdalg.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,20 @@
5454
"""
5555

5656
import scipy as sp
57+
import numpy as np
5758
from . import xferfcn as tf
5859
from . import statesp as ss
5960
from . import frdata as frd
6061

6162
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect']
6263

63-
def series(sys1, sys2):
64-
"""Return the series connection sys2 * sys1 for --> sys1 --> sys2 -->.
64+
def series(sys1, *sysn):
65+
"""Return the series connection (... * sys3 *) sys2 * sys1
6566
6667
Parameters
6768
----------
6869
sys1: scalar, StateSpace, TransferFunction, or FRD
69-
sys2: scalar, StateSpace, TransferFunction, or FRD
70+
*sysn: other scalers, StateSpaces, TransferFunctions, or FRDs
7071
7172
Returns
7273
-------
@@ -96,20 +97,22 @@ def series(sys1, sys2):
9697
9798
Examples
9899
--------
99-
>>> sys3 = series(sys1, sys2) # Same as sys3 = sys2 * sys1.
100+
>>> sys3 = series(sys1, sys2) # Same as sys3 = sys2 * sys1
100101
101-
"""
102+
>>> sys5 = series(sys1, sys2, sys3, sys4) # More systems
102103
103-
return sys2 * sys1
104+
"""
105+
from functools import reduce
106+
return reduce(lambda x, y:x*y, sysn, sys1)
104107

105-
def parallel(sys1, sys2):
108+
def parallel(sys1, *sysn):
106109
"""
107-
Return the parallel connection sys1 + sys2.
110+
Return the parallel connection sys1 + sys2 (+ sys3 + ...)
108111
109112
Parameters
110113
----------
111114
sys1: scalar, StateSpace, TransferFunction, or FRD
112-
sys2: scalar, StateSpace, TransferFunction, or FRD
115+
*sysn: other scalers, StateSpaces, TransferFunctions, or FRDs
113116
114117
Returns
115118
-------
@@ -139,11 +142,13 @@ def parallel(sys1, sys2):
139142
140143
Examples
141144
--------
142-
>>> sys3 = parallel(sys1, sys2) # Same as sys3 = sys1 + sys2.
145+
>>> sys3 = parallel(sys1, sys2) # Same as sys3 = sys1 + sys2
143146
144-
"""
147+
>>> sys5 = parallel(sys1, sys2, sys3, sys4) # More systems
145148
146-
return sys1 + sys2
149+
"""
150+
from functools import reduce
151+
return reduce(lambda x, y:x+y, sysn, sys1)
147152

148153
def negate(sys):
149154
"""
@@ -221,18 +226,18 @@ def feedback(sys1, sys2=1, sign=-1):
221226
"""
222227

223228
# Check for correct input types.
224-
if not isinstance(sys1, (int, float, complex, tf.TransferFunction,
225-
ss.StateSpace, frd.FRD)):
229+
if not isinstance(sys1, (int, float, complex, np.number,
230+
tf.TransferFunction, ss.StateSpace, frd.FRD)):
226231
raise TypeError("sys1 must be a TransferFunction, StateSpace " +
227232
"or FRD object, or a scalar.")
228-
if not isinstance(sys2, (int, float, complex, tf.TransferFunction,
229-
ss.StateSpace, frd.FRD)):
233+
if not isinstance(sys2, (int, float, complex, np.number,
234+
tf.TransferFunction, ss.StateSpace, frd.FRD)):
230235
raise TypeError("sys2 must be a TransferFunction, StateSpace " +
231236
"or FRD object, or a scalar.")
232237

233238
# If sys1 is a scalar, convert it to the appropriate LTI type so that we can
234239
# its feedback member function.
235-
if isinstance(sys1, (int, float, complex)):
240+
if isinstance(sys1, (int, float, complex, np.number)):
236241
if isinstance(sys2, tf.TransferFunction):
237242
sys1 = tf._convertToTransferFunction(sys1)
238243
elif isinstance(sys2, ss.StateSpace):
@@ -246,7 +251,8 @@ def feedback(sys1, sys2=1, sign=-1):
246251
return sys1.feedback(sys2, sign)
247252

248253
def append(*sys):
249-
'''
254+
'''append(sys1, sys2, ..., sysn)
255+
250256
Group models by appending their inputs and outputs
251257
252258
Forms an augmented system model, and appends the inputs and

control/ctrlutil.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@
4343
# Packages that we need access to
4444
from . import lti
4545
import numpy as np
46-
from numpy import pi
46+
import math
4747

4848
__all__ = ['unwrap', 'issys', 'db2mag', 'mag2db']
4949

5050
# Utility function to unwrap an angle measurement
51-
def unwrap(angle, period=2*pi):
51+
def unwrap(angle, period=2*math.pi):
5252
"""Unwrap a phase angle to give a continuous curve
5353
5454
Parameters

control/frdata.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"""
5050

5151
# External function declarations
52+
import numpy as np
5253
from numpy import angle, array, empty, ones, \
5354
real, imag, matrix, absolute, eye, linalg, where, dot
5455
from scipy.interpolate import splprep, splev
@@ -57,7 +58,9 @@
5758
__all__ = ['FRD', 'frd']
5859

5960
class FRD(LTI):
60-
"""A class for models defined by Frequency Response Data (FRD)
61+
"""FRD(d, w)
62+
63+
A class for models defined by frequency response data (FRD)
6164
6265
The FRD class is used to represent systems in frequency response data form.
6366
@@ -80,7 +83,9 @@ class FRD(LTI):
8083
epsw = 1e-8
8184

8285
def __init__(self, *args, **kwargs):
83-
"""Construct an FRD object
86+
"""FRD(d, w)
87+
88+
Construct an FRD object
8489
8590
The default constructor is FRD(d, w), where w is an iterable of
8691
frequency points, and d is the matching frequency data.
@@ -111,7 +116,7 @@ def __init__(self, *args, **kwargs):
111116
(otherlti.outputs, otherlti.inputs, numfreq),
112117
dtype=complex)
113118
for k, w in enumerate(self.omega):
114-
self.fresp[:, :, k] = otherlti.evalfr(w)
119+
self.fresp[:, :, k] = otherlti._evalfr(w)
115120

116121
else:
117122
# The user provided a response and a freq vector
@@ -219,7 +224,7 @@ def __mul__(self, other):
219224
"""Multiply two LTI objects (serial connection)."""
220225

221226
# Convert the second argument to a transfer function.
222-
if isinstance(other, (int, float, complex)):
227+
if isinstance(other, (int, float, complex, np.number)):
223228
return FRD(self.fresp * other, self.omega,
224229
smooth=(self.ifunc is not None))
225230
else:
@@ -245,7 +250,7 @@ def __rmul__(self, other):
245250
"""Right Multiply two LTI objects (serial connection)."""
246251

247252
# Convert the second argument to an frd function.
248-
if isinstance(other, (int, float, complex)):
253+
if isinstance(other, (int, float, complex, np.number)):
249254
return FRD(self.fresp * other, self.omega,
250255
smooth=(self.ifunc is not None))
251256
else:
@@ -272,7 +277,7 @@ def __rmul__(self, other):
272277
def __truediv__(self, other):
273278
"""Divide two LTI objects."""
274279

275-
if isinstance(other, (int, float, complex)):
280+
if isinstance(other, (int, float, complex, np.number)):
276281
return FRD(self.fresp * (1/other), self.omega,
277282
smooth=(self.ifunc is not None))
278283
else:
@@ -295,7 +300,7 @@ def __div__(self, other):
295300
# TODO: Division of MIMO transfer function objects is not written yet.
296301
def __rtruediv__(self, other):
297302
"""Right divide two LTI objects."""
298-
if isinstance(other, (int, float, complex)):
303+
if isinstance(other, (int, float, complex, np.number)):
299304
return FRD(other / self.fresp, self.omega,
300305
smooth=(self.ifunc is not None))
301306
else:
@@ -324,19 +329,42 @@ def __pow__(self,other):
324329
return (FRD(ones(self.fresp.shape), self.omega) / self) * \
325330
(self**(other+1))
326331

327-
328332
def evalfr(self, omega):
329333
"""Evaluate a transfer function at a single angular frequency.
330334
331-
self.evalfr(omega) returns the value of the frequency response
335+
self._evalfr(omega) returns the value of the frequency response
336+
at frequency omega.
337+
338+
Note that a "normal" FRD only returns values for which there is an
339+
entry in the omega vector. An interpolating FRD can return
340+
intermediate values.
341+
342+
"""
343+
warn("FRD.evalfr(omega) will be deprecated in a future release of python-control; use sys.eval(omega) instead",
344+
PendingDeprecationWarning)
345+
return self._evalfr(omega)
346+
347+
# Define the `eval` function to evaluate an FRD at a given (real)
348+
# frequency. Note that we choose to use `eval` instead of `evalfr` to
349+
# avoid confusion with :func:`evalfr`, which takes a complex number as its
350+
# argument. Similarly, we don't use `__call__` to avoid confusion between
351+
# G(s) for a transfer function and G(omega) for an FRD object.
352+
def eval(self, omega):
353+
"""Evaluate a transfer function at a single angular frequency.
354+
355+
self._evalfr(omega) returns the value of the frequency response
332356
at frequency omega.
333357
334358
Note that a "normal" FRD only returns values for which there is an
335359
entry in the omega vector. An interpolating FRD can return
336360
intermediate values.
337361
338362
"""
363+
return self._evalfr(omega)
339364

365+
# Internal function to evaluate the frequency responses
366+
def _evalfr(self, omega):
367+
"""Evaluate a transfer function at a single angular frequency."""
340368
# Preallocate the output.
341369
if getattr(omega, '__iter__', False):
342370
out = empty((self.outputs, self.inputs, len(omega)), dtype=complex)
@@ -385,7 +413,7 @@ def freqresp(self, omega):
385413
omega.sort()
386414

387415
for k, w in enumerate(omega):
388-
fresp = self.evalfr(w)
416+
fresp = self._evalfr(w)
389417
mag[:, :, k] = abs(fresp)
390418
phase[:, :, k] = angle(fresp)
391419

@@ -445,11 +473,11 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
445473
omega.sort()
446474
fresp = empty((sys.outputs, sys.inputs, len(omega)), dtype=complex)
447475
for k, w in enumerate(omega):
448-
fresp[:, :, k] = sys.evalfr(w)
476+
fresp[:, :, k] = sys._evalfr(w)
449477

450478
return FRD(fresp, omega, smooth=True)
451479

452-
elif isinstance(sys, (int, float, complex)):
480+
elif isinstance(sys, (int, float, complex, np.number)):
453481
fresp = ones((outputs, inputs, len(omega)), dtype=float)*sys
454482
return FRD(fresp, omega, smooth=True)
455483

@@ -469,8 +497,9 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
469497
sys.__class__)
470498

471499
def frd(*args):
472-
"""
473-
Construct a Frequency Response Data model, or convert a system
500+
"""frd(d, w)
501+
502+
Construct a frequency response data model
474503
475504
frd models store the (measured) frequency response of a system.
476505
@@ -500,6 +529,6 @@ def frd(*args):
500529
501530
See Also
502531
--------
503-
ss, tf
532+
FRD, ss, tf
504533
"""
505534
return FRD(*args)

0 commit comments

Comments
 (0)