Skip to content

Commit fcf7b85

Browse files
committed
2 parents 2a6143f + 10e8f47 commit fcf7b85

File tree

14 files changed

+1110
-1298
lines changed

14 files changed

+1110
-1298
lines changed

control/bdalg.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@
6161

6262
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect']
6363

64-
def series(sys1, sys2):
65-
"""Return the series connection sys2 * sys1 for --> sys1 --> sys2 -->.
64+
def series(sys1, *sysn):
65+
"""Return the series connection (... * sys3 *) sys2 * sys1
6666
6767
Parameters
6868
----------
6969
sys1: scalar, StateSpace, TransferFunction, or FRD
70-
sys2: scalar, StateSpace, TransferFunction, or FRD
70+
*sysn: other scalers, StateSpaces, TransferFunctions, or FRDs
7171
7272
Returns
7373
-------
@@ -97,20 +97,22 @@ def series(sys1, sys2):
9797
9898
Examples
9999
--------
100-
>>> sys3 = series(sys1, sys2) # Same as sys3 = sys2 * sys1.
100+
>>> sys3 = series(sys1, sys2) # Same as sys3 = sys2 * sys1
101101
102-
"""
102+
>>> sys5 = series(sys1, sys2, sys3, sys4) # More systems
103103
104-
return sys2 * sys1
104+
"""
105+
from functools import reduce
106+
return reduce(lambda x, y:x*y, sysn, sys1)
105107

106-
def parallel(sys1, sys2):
108+
def parallel(sys1, *sysn):
107109
"""
108-
Return the parallel connection sys1 + sys2.
110+
Return the parallel connection sys1 + sys2 (+ sys3 + ...)
109111
110112
Parameters
111113
----------
112114
sys1: scalar, StateSpace, TransferFunction, or FRD
113-
sys2: scalar, StateSpace, TransferFunction, or FRD
115+
*sysn: other scalers, StateSpaces, TransferFunctions, or FRDs
114116
115117
Returns
116118
-------
@@ -140,11 +142,13 @@ def parallel(sys1, sys2):
140142
141143
Examples
142144
--------
143-
>>> sys3 = parallel(sys1, sys2) # Same as sys3 = sys1 + sys2.
145+
>>> sys3 = parallel(sys1, sys2) # Same as sys3 = sys1 + sys2
144146
145-
"""
147+
>>> sys5 = parallel(sys1, sys2, sys3, sys4) # More systems
146148
147-
return sys1 + sys2
149+
"""
150+
from functools import reduce
151+
return reduce(lambda x, y:x+y, sysn, sys1)
148152

149153
def negate(sys):
150154
"""
@@ -247,7 +251,7 @@ def feedback(sys1, sys2=1, sign=-1):
247251
return sys1.feedback(sys2, sign)
248252

249253
def append(*sys):
250-
'''append(sys1, sys2, ... sysn)
254+
'''append(sys1, sys2, ..., sysn)
251255
252256
Group models by appending their inputs and outputs
253257

control/frdata.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def __init__(self, *args, **kwargs):
116116
(otherlti.outputs, otherlti.inputs, numfreq),
117117
dtype=complex)
118118
for k, w in enumerate(self.omega):
119-
self.fresp[:, :, k] = otherlti.evalfr(w)
119+
self.fresp[:, :, k] = otherlti._evalfr(w)
120120

121121
else:
122122
# The user provided a response and a freq vector
@@ -329,19 +329,42 @@ def __pow__(self,other):
329329
return (FRD(ones(self.fresp.shape), self.omega) / self) * \
330330
(self**(other+1))
331331

332-
333332
def evalfr(self, omega):
334333
"""Evaluate a transfer function at a single angular frequency.
335334
336-
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
337356
at frequency omega.
338357
339358
Note that a "normal" FRD only returns values for which there is an
340359
entry in the omega vector. An interpolating FRD can return
341360
intermediate values.
342361
343362
"""
363+
return self._evalfr(omega)
344364

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

392415
for k, w in enumerate(omega):
393-
fresp = self.evalfr(w)
416+
fresp = self._evalfr(w)
394417
mag[:, :, k] = abs(fresp)
395418
phase[:, :, k] = angle(fresp)
396419

@@ -450,7 +473,7 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
450473
omega.sort()
451474
fresp = empty((sys.outputs, sys.inputs, len(omega)), dtype=complex)
452475
for k, w in enumerate(omega):
453-
fresp[:, :, k] = sys.evalfr(w)
476+
fresp[:, :, k] = sys._evalfr(w)
454477

455478
return FRD(fresp, omega, smooth=True)
456479

control/margins.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,10 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
164164
# test (imaginary part of tf) == 0, for phase crossover/gain margins
165165
test_w_180 = np.polyadd(np.polymul(inum, rden), np.polymul(rnum, -iden))
166166
w_180 = np.roots(test_w_180)
167-
#print ('1:w_180', w_180)
168167

169168
# first remove imaginary and negative frequencies, epsw removes the
170169
# "0" frequency for type-2 systems
171170
w_180 = np.real(w_180[(np.imag(w_180) == 0) * (w_180 >= epsw)])
172-
#print ('2:w_180', w_180)
173171

174172
# evaluate response at remaining frequencies, to test for phase 180 vs 0
175173
with np.errstate(all='ignore'):
@@ -182,7 +180,6 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
182180

183181
# and sort
184182
w_180.sort()
185-
#print ('3:w_180', w_180)
186183

187184
# test magnitude is 1 for gain crossover/phase margins
188185
test_wc = np.polysub(np.polyadd(_polysqr(rnum), _polysqr(inum)),
@@ -203,32 +200,28 @@ def stability_margins(sysdata, returnall=False, epsw=0.0):
203200

204201
# find the solutions, for positive omega, and only real ones
205202
wstab = np.roots(test_wstab)
206-
#print('wstabr', wstab)
207203
wstab = np.real(wstab[(np.imag(wstab) == 0) *
208204
(np.real(wstab) >= 0)])
209-
#print('wstab', wstab)
210205

211206
# and find the value of the 2nd derivative there, needs to be positive
212207
wstabplus = np.polyval(np.polyder(test_wstab), wstab)
213-
#print('wstabplus', wstabplus)
214208
wstab = np.real(wstab[(np.imag(wstab) == 0) * (wstab > epsw) *
215209
(wstabplus > 0.)])
216-
#print('wstab', wstab)
217210
wstab.sort()
218211

219212
else:
220213
# a bit coarse, have the interpolated frd evaluated again
221214
def mod(w):
222215
"""to give the function to calculate |G(jw)| = 1"""
223-
return np.abs(sys.evalfr(w)[0][0]) - 1
216+
return np.abs(sys._evalfr(w)[0][0]) - 1
224217

225218
def arg(w):
226219
"""function to calculate the phase angle at -180 deg"""
227-
return np.angle(-sys.evalfr(w)[0][0])
220+
return np.angle(-sys._evalfr(w)[0][0])
228221

229222
def dstab(w):
230223
"""function to calculate the distance from -1 point"""
231-
return np.abs(sys.evalfr(w)[0][0] + 1.)
224+
return np.abs(sys._evalfr(w)[0][0] + 1.)
232225

233226
# Find all crossings, note that this depends on omega having
234227
# a correct range
@@ -239,37 +232,28 @@ def dstab(w):
239232

240233
# find the phase crossings ang(H(jw) == -180
241234
widx = np.where(np.diff(np.sign(arg(sys.omega))))[0]
242-
#print('widx (180)', widx, sys.omega[widx])
243-
#print('x', sys.evalfr(sys.omega[widx])[0][0])
244-
widx = widx[np.real(sys.evalfr(sys.omega[widx])[0][0]) <= 0]
245-
#print('widx (180,2)', widx)
235+
widx = widx[np.real(sys._evalfr(sys.omega[widx])[0][0]) <= 0]
246236
w_180 = np.array(
247237
[ sp.optimize.brentq(arg, sys.omega[i], sys.omega[i+1])
248238
for i in widx if i+1 < len(sys.omega) ])
249-
#print('x', sys.evalfr(w_180)[0][0])
250-
#print('w_180', w_180)
251239

252240
# find all stab margins?
253241
widx = np.where(np.diff(np.sign(np.diff(dstab(sys.omega)))))[0]
254-
#print('widx', widx)
255-
#print('wstabx', sys.omega[widx])
256242
wstab = np.array([ sp.optimize.minimize_scalar(
257243
dstab, bracket=(sys.omega[i], sys.omega[i+1])).x
258244
for i in widx if i+1 < len(sys.omega) and
259245
np.diff(np.diff(dstab(sys.omega[i-1:i+2])))[0] > 0 ])
260-
#print('wstabf0', wstab)
261246
wstab = wstab[(wstab >= sys.omega[0]) *
262247
(wstab <= sys.omega[-1])]
263-
#print ('wstabf', wstab)
264248

265249

266250
# margins, as iterables, converted frdata and xferfcn calculations to
267251
# vector for this
268252
with np.errstate(all='ignore'):
269-
gain_w_180 = np.abs(sys.evalfr(w_180)[0][0])
253+
gain_w_180 = np.abs(sys._evalfr(w_180)[0][0])
270254
GM = 1.0/gain_w_180
271-
SM = np.abs(sys.evalfr(wstab)[0][0]+1)
272-
PM = np.remainder(np.angle(sys.evalfr(wc)[0][0], deg=True), 360.0) - 180.0
255+
SM = np.abs(sys._evalfr(wstab)[0][0]+1)
256+
PM = np.remainder(np.angle(sys._evalfr(wc)[0][0], deg=True), 360.0) - 180.0
273257

274258
if returnall:
275259
return GM, PM, SM, w_180, wc, wstab
@@ -331,7 +315,7 @@ def phase_crossover_frequencies(sys):
331315

332316
# using real() to avoid rounding errors and results like 1+0j
333317
# it would be nice to have a vectorized version of self.evalfr here
334-
gain = np.real(np.asarray([tf.evalfr(f)[0][0] for f in realposfreq]))
318+
gain = np.real(np.asarray([tf._evalfr(f)[0][0] for f in realposfreq]))
335319

336320
return realposfreq, gain
337321

0 commit comments

Comments
 (0)