Skip to content

Commit 7b2defe

Browse files
authored
Merge pull request #173 from rabraker/fresp_tb05ad
Use slycot's tb05ad when available for faster and more accurate frequency response
2 parents 7dcb892 + 62a29d9 commit 7b2defe

File tree

1 file changed

+86
-10
lines changed

1 file changed

+86
-10
lines changed

control/statesp.py

+86-10
Original file line numberDiff line numberDiff line change
@@ -387,21 +387,97 @@ def horner(self, s):
387387

388388
# Method for generating the frequency response of the system
389389
def freqresp(self, omega):
390-
"""Evaluate the system's transfer func. at a list of ang. frequencies.
390+
"""Evaluate the system's transfer func. at a list of freqs, omega.
391391
392392
mag, phase, omega = self.freqresp(omega)
393393
394-
reports the value of the magnitude, phase, and angular frequency of the
395-
system's transfer function matrix evaluated at s = i * omega, where
396-
omega is a list of angular frequencies, and is a sorted version of the
397-
input omega.
394+
Reports the frequency response of the system,
395+
396+
G(j*omega) = mag*exp(j*phase)
397+
398+
for continuous time. For discrete time systems, the response is
399+
evaluated around the unit circle such that
400+
401+
G(exp(j*omega*dt)) = mag*exp(j*phase).
402+
403+
Inputs:
404+
------
405+
omega: A list of frequencies in radians/sec at which the system
406+
should be evaluated. The list can be either a python list
407+
or a numpy array and will be sorted before evaluation.
408+
409+
Returns:
410+
-------
411+
mag: The magnitude (absolute value, not dB or log10) of the system
412+
frequency response.
413+
414+
phase: The wrapped phase in radians of the system frequency
415+
response.
416+
417+
omega: The list of sorted frequencies at which the response
418+
was evaluated.
398419
399420
"""
400-
# when evaluating at many frequencies, much faster to convert to
401-
# transfer function first and then evaluate, than to solve an
402-
# n-dimensional linear system at each frequency
403-
tf = _convertToTransferFunction(self)
404-
return tf.freqresp(omega)
421+
422+
# In case omega is passed in as a list, rather than a proper array.
423+
omega = np.asarray(omega)
424+
425+
numFreqs = len(omega)
426+
Gfrf = np.empty((self.outputs, self.inputs, numFreqs),
427+
dtype=np.complex128)
428+
429+
# Sort frequency and calculate complex frequencies on either imaginary
430+
# axis (continuous time) or unit circle (discrete time).
431+
omega.sort()
432+
if isdtime(self, strict=True):
433+
dt = timebase(self)
434+
cmplx_freqs = exp(1.j * omega * dt)
435+
if ((omega * dt).any() > pi):
436+
warn_message = ("evalfr: frequency evaluation"
437+
" above Nyquist frequency")
438+
warnings.warn(warn_message)
439+
else:
440+
cmplx_freqs = omega * 1.j
441+
442+
# Do the frequency response evaluation. Use TB05AD from Slycot
443+
# if it's available, otherwise use the built-in horners function.
444+
try:
445+
from slycot import tb05ad
446+
447+
n = np.shape(self.A)[0]
448+
m = self.inputs
449+
p = self.outputs
450+
# The first call both evalates C(sI-A)^-1 B and also returns
451+
# hessenberg transformed matrices at, bt, ct.
452+
result = tb05ad(n, m, p, cmplx_freqs[0], self.A,
453+
self.B, self.C, job='NG')
454+
# When job='NG', result = (at, bt, ct, g_i, hinvb, info)
455+
at = result[0]
456+
bt = result[1]
457+
ct = result[2]
458+
459+
# TB05AD freqency evaluation does not include direct feedthrough.
460+
Gfrf[:, :, 0] = result[3] + self.D
461+
462+
# Now, iterate through the remaining frequencies using the
463+
# transformed state matrices, at, bt, ct.
464+
465+
# Start at the second frequency, already have the first.
466+
for kk, cmplx_freqs_kk in enumerate(cmplx_freqs[1:numFreqs]):
467+
result = tb05ad(n, m, p, cmplx_freqs_kk, at,
468+
bt, ct, job='NH')
469+
# When job='NH', result = (g_i, hinvb, info)
470+
471+
# kk+1 because enumerate starts at kk = 0.
472+
# but zero-th spot is already filled.
473+
Gfrf[:, :, kk+1] = result[0] + self.D
474+
475+
except ImportError: # Slycot unavailable. Fall back to horner.
476+
for kk, cmplx_freqs_kk in enumerate(cmplx_freqs):
477+
Gfrf[:, :, kk] = self.horner(cmplx_freqs_kk)
478+
479+
# mag phase omega
480+
return np.abs(Gfrf), np.angle(Gfrf), omega
405481

406482
# Compute poles and zeros
407483
def pole(self):

0 commit comments

Comments
 (0)