Skip to content

Commit 8c957b2

Browse files
authored
Merge branch 'master' into interconnectedsystems
2 parents 3ec2f02 + d3142ff commit 8c957b2

32 files changed

+1344
-741
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/dtime.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
__all__ = ['sample_system', 'c2d']
5353

5454
# Sample a continuous time system
55-
def sample_system(sysc, Ts, method='zoh', alpha=None):
55+
def sample_system(sysc, Ts, method='zoh', alpha=None, prewarp_frequency=None):
5656
"""Convert a continuous time system to discrete time
5757
5858
Creates a discrete time system from a continuous time system by
@@ -67,6 +67,10 @@ def sample_system(sysc, Ts, method='zoh', alpha=None):
6767
method : string
6868
Method to use for conversion: 'matched', 'tustin', 'zoh' (default)
6969
70+
prewarp_frequency : float within [0, infinity)
71+
The frequency [rad/s] at which to match with the input continuous-
72+
time system's magnitude and phase
73+
7074
Returns
7175
-------
7276
sysd : linsys
@@ -87,10 +91,10 @@ def sample_system(sysc, Ts, method='zoh', alpha=None):
8791
if not isctime(sysc):
8892
raise ValueError("First argument must be continuous time system")
8993

90-
return sysc.sample(Ts, method, alpha)
94+
return sysc.sample(Ts, method, alpha, prewarp_frequency)
9195

9296

93-
def c2d(sysc, Ts, method='zoh'):
97+
def c2d(sysc, Ts, method='zoh', prewarp_frequency=None):
9498
'''
9599
Return a discrete-time system
96100
@@ -109,9 +113,14 @@ def c2d(sysc, Ts, method='zoh'):
109113
'impulse' Impulse-invariant discretization, currently not implemented
110114
'tustin' Bilinear (Tustin) approximation, only SISO
111115
'matched' Matched pole-zero method, only SISO
116+
117+
prewarp_frequency : float within [0, infinity)
118+
The frequency [rad/s] at which to match with the input continuous-
119+
time system's magnitude and phase
120+
112121
'''
113122
# Call the sample_system() function to do the work
114-
sysd = sample_system(sysc, Ts, method)
123+
sysd = sample_system(sysc, Ts, method, prewarp_frequency)
115124

116125
# TODO: is this check needed? If sysc is StateSpace, sysd is too?
117126
if isinstance(sysc, StateSpace) and not isinstance(sysd, StateSpace):

control/frdata.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,14 @@ def __str__(self):
161161
"""String representation of the transfer function."""
162162

163163
mimo = self.inputs > 1 or self.outputs > 1
164-
outstr = ['frequency response data ']
164+
outstr = ['Frequency response data']
165165

166166
mt, pt, wt = self.freqresp(self.omega)
167167
for i in range(self.inputs):
168168
for j in range(self.outputs):
169169
if mimo:
170170
outstr.append("Input %i to output %i:" % (i + 1, j + 1))
171-
outstr.append('Freq [rad/s] Response ')
171+
outstr.append('Freq [rad/s] Response')
172172
outstr.append('------------ ---------------------')
173173
outstr.extend(
174174
['%12.3f %10.4g%+10.4gj' % (w, m, p)
@@ -177,6 +177,15 @@ def __str__(self):
177177

178178
return '\n'.join(outstr)
179179

180+
def __repr__(self):
181+
"""Loadable string representation,
182+
183+
limited for number of data points.
184+
"""
185+
return "FrequencyResponseData({d}, {w}{smooth})".format(
186+
d=repr(self.fresp), w=repr(self.omega),
187+
smooth=(self.ifunc and ", smooth=True") or "")
188+
180189
def __neg__(self):
181190
"""Negate a transfer function."""
182191

@@ -400,17 +409,30 @@ def _evalfr(self, omega):
400409

401410
# Method for generating the frequency response of the system
402411
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-
412+
"""Evaluate the frequency response at a list of angular frequencies.
413+
414+
Reports the value of the magnitude, phase, and angular frequency of
415+
the requency response evaluated at omega, where omega is a list of
416+
angular frequencies, and is a sorted version of the input omega.
417+
418+
Parameters
419+
----------
420+
omega : array_like
421+
A list of frequencies in radians/sec at which the system should be
422+
evaluated. The list can be either a python list or a numpy array
423+
and will be sorted before evaluation.
424+
425+
Returns
426+
-------
427+
mag : (self.outputs, self.inputs, len(omega)) ndarray
428+
The magnitude (absolute value, not dB or log10) of the system
429+
frequency response.
430+
phase : (self.outputs, self.inputs, len(omega)) ndarray
431+
The wrapped phase in radians of the system frequency response.
432+
omega : ndarray or list or tuple
433+
The list of sorted frequencies at which the response was
434+
evaluated.
412435
"""
413-
414436
# Preallocate outputs.
415437
numfreq = len(omega)
416438
mag = empty((self.outputs, self.inputs, numfreq))

control/freqplot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -822,10 +822,10 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None,
822822

823823
# Set the range to be an order of magnitude beyond any features
824824
if number_of_samples:
825-
omega = sp.logspace(
825+
omega = np.logspace(
826826
lsp_min, lsp_max, num=number_of_samples, endpoint=True)
827827
else:
828-
omega = sp.logspace(lsp_min, lsp_max, endpoint=True)
828+
omega = np.logspace(lsp_min, lsp_max, endpoint=True)
829829
return omega
830830

831831

control/grid.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,12 @@ def nogrid():
136136
return ax, f
137137

138138

139-
def zgrid(zetas=None, wns=None):
139+
def zgrid(zetas=None, wns=None, ax=None):
140140
'''Draws discrete damping and frequency grid'''
141141

142142
fig = plt.gcf()
143-
ax = fig.gca()
143+
if ax is None:
144+
ax = fig.gca()
144145

145146
# Constant damping lines
146147
if zetas is None:
@@ -154,11 +155,11 @@ def zgrid(zetas=None, wns=None):
154155
# Draw upper part in retangular coordinates
155156
xret = mag*cos(ang)
156157
yret = mag*sin(ang)
157-
ax.plot(xret, yret, 'k:', lw=1)
158+
ax.plot(xret, yret, ':', color='grey', lw=0.75)
158159
# Draw lower part in retangular coordinates
159160
xret = mag*cos(-ang)
160161
yret = mag*sin(-ang)
161-
ax.plot(xret, yret, 'k:', lw=1)
162+
ax.plot(xret, yret, ':', color='grey', lw=0.75)
162163
# Annotation
163164
an_i = int(len(xret)/2.5)
164165
an_x = xret[an_i]
@@ -177,7 +178,7 @@ def zgrid(zetas=None, wns=None):
177178
# Draw in retangular coordinates
178179
xret = mag*cos(ang)
179180
yret = mag*sin(ang)
180-
ax.plot(xret, yret, 'k:', lw=1)
181+
ax.plot(xret, yret, ':', color='grey', lw=0.75)
181182
# Annotation
182183
an_i = -1
183184
an_x = xret[an_i]

control/iosys.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,8 +1663,10 @@ def rootfun(z):
16631663
# and were processed above.
16641664

16651665
# Get the states and inputs that were not listed as fixed
1666-
state_vars = np.delete(np.array(range(nstates)), ix)
1667-
input_vars = np.delete(np.array(range(ninputs)), iu)
1666+
state_vars = (range(nstates) if not len(ix)
1667+
else np.delete(np.array(range(nstates)), ix))
1668+
input_vars = (range(ninputs) if not len(iu)
1669+
else np.delete(np.array(range(ninputs)), iu))
16681670

16691671
# Set the outputs and derivs that will serve as constraints
16701672
output_vars = np.array(iy)
@@ -1763,16 +1765,23 @@ def linearize(sys, xeq, ueq=[], t=0, params={}, **kw):
17631765
return sys.linearize(xeq, ueq, t=t, params=params, **kw)
17641766

17651767

1766-
# Utility function to find the size of a system parameter
17671768
def _find_size(sysval, vecval):
1768-
if sysval is not None:
1769-
return sysval
1770-
elif hasattr(vecval, '__len__'):
1769+
"""Utility function to find the size of a system parameter
1770+
1771+
If both parameters are not None, they must be consistent.
1772+
"""
1773+
if hasattr(vecval, '__len__'):
1774+
if sysval is not None and sysval != len(vecval):
1775+
raise ValueError("Inconsistend information to determine size "
1776+
"of system component")
17711777
return len(vecval)
1772-
elif vecval is None:
1773-
return 0
1774-
else:
1775-
raise ValueError("Can't determine size of system component.")
1778+
# None or 0, which is a valid value for "a (sysval, ) vector of zeros".
1779+
if not vecval:
1780+
return 0 if sysval is None else sysval
1781+
elif sysval == 1:
1782+
# (1, scalar) is also a valid combination from legacy code
1783+
return 1
1784+
raise ValueError("Can't determine size of system component.")
17761785

17771786

17781787
# Convert a state space system into an input/output system (wrapper)

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

0 commit comments

Comments
 (0)