Skip to content

Commit 5a34fc0

Browse files
authored
Merge branch 'master' into fix-discrete-step-response
2 parents 79951a1 + ce3a231 commit 5a34fc0

18 files changed

+576
-433
lines changed

.travis.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ python:
1919

2020
# Test against multiple version of SciPy, with and without slycot
2121
#
22-
# Because there were significant changes in SciPy between v0 and v1, we
22+
# Because there were significant changes in SciPy between v0 and v1, we
2323
# test against both of these using the Travis CI environment capability
2424
#
2525
# We also want to test with and without slycot
@@ -84,7 +84,6 @@ before_install:
8484
sudo apt-get update -qq;
8585
sudo apt-get install liblapack-dev libblas-dev;
8686
sudo apt-get install gfortran;
87-
sudo apt-get install cmake;
8887
fi
8988
# use miniconda to install numpy/scipy, to avoid lengthy build from source
9089
- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
@@ -104,7 +103,7 @@ before_install:
104103
# Install scikit-build for the build process if slycot is being used
105104
- if [[ "$SLYCOT" = "source" ]]; then
106105
conda install openblas;
107-
conda install -c conda-forge scikit-build;
106+
conda install -c conda-forge cmake scikit-build;
108107
fi
109108
# Make sure to look in the right place for python libraries (for slycot)
110109
- export LIBRARY_PATH="$HOME/miniconda/envs/test-environment/lib"
@@ -130,7 +129,7 @@ install:
130129
# command to run tests
131130
script:
132131
- 'if [ $SLYCOT != "" ]; then python -c "import slycot"; fi'
133-
- coverage run -m pytest --disable-warnings control/tests
132+
- coverage run -m pytest control/tests
134133

135134
# only run examples if Slycot is install
136135
# set PYTHONPATH for examples

control/config.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
__all__ = ['defaults', 'set_defaults', 'reset_defaults',
1313
'use_matlab_defaults', 'use_fbs_defaults',
14-
'use_numpy_matrix']
14+
'use_legacy_defaults', 'use_numpy_matrix']
1515

1616
# Package level default values
1717
_control_defaults = {
@@ -53,6 +53,9 @@ def reset_defaults():
5353
from .rlocus import _rlocus_defaults
5454
defaults.update(_rlocus_defaults)
5555

56+
from .xferfcn import _xferfcn_defaults
57+
defaults.update(_xferfcn_defaults)
58+
5659
from .statesp import _statesp_defaults
5760
defaults.update(_statesp_defaults)
5861

@@ -156,3 +159,16 @@ class and functions. If flat is `False`, then matrices are
156159
warnings.warn("Return type numpy.matrix is soon to be deprecated.",
157160
stacklevel=2)
158161
set_defaults('statesp', use_numpy_matrix=flag)
162+
163+
def use_legacy_defaults(version):
164+
""" Sets the defaults to whatever they were in a given release.
165+
166+
Parameters
167+
----------
168+
version : string
169+
version number of the defaults desired. Currently only supports `0.8.3`.
170+
"""
171+
if version == '0.8.3':
172+
use_numpy_matrix(True) # alternatively: set_defaults('statesp', use_numpy_matrix=True)
173+
else:
174+
raise ValueError('''version number not recognized. Possible values are: ['0.8.3']''')

control/frdata.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,30 @@ def _evalfr(self, omega):
400400

401401
# Method for generating the frequency response of the system
402402
def freqresp(self, omega):
403-
"""Evaluate a transfer function at a list of angular frequencies.
404-
405-
mag, phase, omega = self.freqresp(omega)
406-
407-
reports the value of the magnitude, phase, and angular frequency of
408-
the transfer function matrix evaluated at s = i * omega, where omega
409-
is a list of angular frequencies, and is a sorted version of the input
410-
omega.
411-
403+
"""Evaluate the frequency response at a list of angular frequencies.
404+
405+
Reports the value of the magnitude, phase, and angular frequency of
406+
the requency response evaluated at omega, where omega is a list of
407+
angular frequencies, and is a sorted version of the input omega.
408+
409+
Parameters
410+
----------
411+
omega : array_like
412+
A list of frequencies in radians/sec at which the system should be
413+
evaluated. The list can be either a python list or a numpy array
414+
and will be sorted before evaluation.
415+
416+
Returns
417+
-------
418+
mag : (self.outputs, self.inputs, len(omega)) ndarray
419+
The magnitude (absolute value, not dB or log10) of the system
420+
frequency response.
421+
phase : (self.outputs, self.inputs, len(omega)) ndarray
422+
The wrapped phase in radians of the system frequency response.
423+
omega : ndarray or list or tuple
424+
The list of sorted frequencies at which the response was
425+
evaluated.
412426
"""
413-
414427
# Preallocate outputs.
415428
numfreq = len(omega)
416429
mag = empty((self.outputs, self.inputs, numfreq))

control/iosys.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,8 +1645,10 @@ def rootfun(z):
16451645
# and were processed above.
16461646

16471647
# Get the states and inputs that were not listed as fixed
1648-
state_vars = np.delete(np.array(range(nstates)), ix)
1649-
input_vars = np.delete(np.array(range(ninputs)), iu)
1648+
state_vars = (range(nstates) if not len(ix)
1649+
else np.delete(np.array(range(nstates)), ix))
1650+
input_vars = (range(ninputs) if not len(iu)
1651+
else np.delete(np.array(range(ninputs)), iu))
16501652

16511653
# Set the outputs and derivs that will serve as constraints
16521654
output_vars = np.array(iy)

control/lti.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def isdtime(self, strict=False):
5555
Parameters
5656
----------
5757
strict: bool, optional
58-
If strict is True, make sure that timebase is not None. Default
59-
is False.
58+
If strict is True, make sure that timebase is not None. Default
59+
is False.
6060
"""
6161

6262
# If no timebase is given, answer depends on strict flag
@@ -75,8 +75,8 @@ def isctime(self, strict=False):
7575
sys : LTI system
7676
System to be checked
7777
strict: bool, optional
78-
If strict is True, make sure that timebase is not None. Default
79-
is False.
78+
If strict is True, make sure that timebase is not None. Default
79+
is False.
8080
"""
8181
# If no timebase is given, answer depends on strict flag
8282
if self.dt is None:
@@ -421,6 +421,7 @@ def evalfr(sys, x):
421421
return sys.horner(x)[0][0]
422422
return sys.horner(x)
423423

424+
424425
def freqresp(sys, omega):
425426
"""
426427
Frequency response of an LTI system at multiple angular frequencies.
@@ -430,13 +431,20 @@ def freqresp(sys, omega):
430431
sys: StateSpace or TransferFunction
431432
Linear system
432433
omega: array_like
433-
List of frequencies
434+
A list of frequencies in radians/sec at which the system should be
435+
evaluated. The list can be either a python list or a numpy array
436+
and will be sorted before evaluation.
434437
435438
Returns
436439
-------
437-
mag: ndarray
438-
phase: ndarray
439-
omega: list, tuple, or ndarray
440+
mag : (self.outputs, self.inputs, len(omega)) ndarray
441+
The magnitude (absolute value, not dB or log10) of the system
442+
frequency response.
443+
phase : (self.outputs, self.inputs, len(omega)) ndarray
444+
The wrapped phase in radians of the system frequency response.
445+
omega : ndarray or list or tuple
446+
The list of sorted frequencies at which the response was
447+
evaluated.
440448
441449
See Also
442450
--------
@@ -472,9 +480,9 @@ def freqresp(sys, omega):
472480
#>>> # frequency response from the 1st input to the 2nd output, for
473481
#>>> # s = 0.1i, i, 10i.
474482
"""
475-
476483
return sys.freqresp(omega)
477484

485+
478486
def dcgain(sys):
479487
"""Return the zero-frequency (or DC) gain of the given system
480488

control/margins.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
"""margin.py
1+
"""margins.py
22
33
Functions for computing stability margins and related functions.
44
55
Routines in this module:
66
7-
margin.stability_margins
8-
margin.phase_crossover_frequencies
9-
margin.margin
7+
margins.stability_margins
8+
margins.phase_crossover_frequencies
9+
margins.margin
1010
"""
1111

1212
# Python 3 compatibility (needs to go here)
@@ -211,41 +211,40 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
211211

212212
else:
213213
# a bit coarse, have the interpolated frd evaluated again
214-
def mod(w):
215-
"""to give the function to calculate |G(jw)| = 1"""
214+
def _mod(w):
215+
"""Calculate |G(jw)| - 1"""
216216
return np.abs(sys._evalfr(w)[0][0]) - 1
217217

218-
def arg(w):
219-
"""function to calculate the phase angle at -180 deg"""
218+
def _arg(w):
219+
"""Calculate the phase angle at -180 deg"""
220220
return np.angle(-sys._evalfr(w)[0][0])
221221

222-
def dstab(w):
223-
"""function to calculate the distance from -1 point"""
222+
def _dstab(w):
223+
"""Calculate the distance from -1 point"""
224224
return np.abs(sys._evalfr(w)[0][0] + 1.)
225225

226226
# Find all crossings, note that this depends on omega having
227227
# a correct range
228-
widx = np.where(np.diff(np.sign(mod(sys.omega))))[0]
228+
widx = np.where(np.diff(np.sign(_mod(sys.omega))))[0]
229229
wc = np.array(
230-
[ sp.optimize.brentq(mod, sys.omega[i], sys.omega[i+1])
231-
for i in widx if i+1 < len(sys.omega)])
230+
[sp.optimize.brentq(_mod, sys.omega[i], sys.omega[i+1])
231+
for i in widx])
232232

233233
# find the phase crossings ang(H(jw) == -180
234-
widx = np.where(np.diff(np.sign(arg(sys.omega))))[0]
234+
widx = np.where(np.diff(np.sign(_arg(sys.omega))))[0]
235235
widx = widx[np.real(sys._evalfr(sys.omega[widx])[0][0]) <= 0]
236236
w_180 = np.array(
237-
[ sp.optimize.brentq(arg, sys.omega[i], sys.omega[i+1])
238-
for i in widx if i+1 < len(sys.omega) ])
237+
[sp.optimize.brentq(_arg, sys.omega[i], sys.omega[i+1])
238+
for i in widx])
239239

240240
# find all stab margins?
241-
widx = np.where(np.diff(np.sign(np.diff(dstab(sys.omega)))))[0]
242-
wstab = np.array([ sp.optimize.minimize_scalar(
243-
dstab, bracket=(sys.omega[i], sys.omega[i+1])).x
244-
for i in widx if i+1 < len(sys.omega) and
245-
np.diff(np.diff(dstab(sys.omega[i-1:i+2])))[0] > 0 ])
246-
wstab = wstab[(wstab >= sys.omega[0]) *
247-
(wstab <= sys.omega[-1])]
248-
241+
widx, = np.where(np.diff(np.sign(np.diff(_dstab(sys.omega)))) > 0)
242+
wstab = np.array(
243+
[sp.optimize.minimize_scalar(_dstab,
244+
bracket=(sys.omega[i], sys.omega[i+1])
245+
).x
246+
for i in widx])
247+
wstab = wstab[(wstab >= sys.omega[0]) * (wstab <= sys.omega[-1])]
249248

250249
# margins, as iterables, converted frdata and xferfcn calculations to
251250
# vector for this
@@ -254,13 +253,13 @@ def dstab(w):
254253
GM = 1.0/gain_w_180
255254
SM = np.abs(sys._evalfr(wstab)[0][0]+1)
256255
PM = np.remainder(np.angle(sys._evalfr(wc)[0][0], deg=True), 360.0) - 180.0
257-
256+
258257
if returnall:
259258
return GM, PM, SM, w_180, wc, wstab
260259
else:
261260
if GM.shape[0] and not np.isinf(GM).all():
262261
with np.errstate(all='ignore'):
263-
gmidx = np.where(np.abs(np.log(GM)) ==
262+
gmidx = np.where(np.abs(np.log(GM)) ==
264263
np.min(np.abs(np.log(GM))))
265264
else:
266265
gmidx = -1
@@ -276,7 +275,6 @@ def dstab(w):
276275

277276

278277
# Contributed by Steffen Waldherr <waldherr@ist.uni-stuttgart.de>
279-
#! TODO - need to add test functions
280278
def phase_crossover_frequencies(sys):
281279
"""Compute frequencies and gains at intersections with real axis
282280
in Nyquist plot.

0 commit comments

Comments
 (0)