Skip to content

use __call__ instead of evalfr in lti system classes #449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1a6d806
eliminate _evalfr in lti system classes, replaced with __call__, whic…
sawyerbfuller Aug 14, 2020
b05492c
fixed remaining failing unit tests and cleanup of docfiles
sawyerbfuller Aug 16, 2020
2c4ac62
minor fix following suggested change in frd str method
sawyerbfuller Aug 16, 2020
6213a54
fixed a few bugs that slipped through
sawyerbfuller Aug 16, 2020
8486b7a
Update control/frdata.py
sawyerbfuller Aug 17, 2020
c8bf92f
Update control/frdata.py
sawyerbfuller Aug 17, 2020
371986c
Update control/lti.py
sawyerbfuller Aug 17, 2020
d8fbaa0
Update control/statesp.py
sawyerbfuller Aug 17, 2020
17bc2a8
Apply suggestions from code review
sawyerbfuller Aug 17, 2020
4ad33db
suggested changes in docstrings
sawyerbfuller Aug 17, 2020
94322c8
Merge remote-tracking branch 'origin/call-method' into call-method
sawyerbfuller Aug 17, 2020
60433e0
better docstrings conforming to numpydoc
sawyerbfuller Aug 17, 2020
262fde6
converted statesp.horner to take care of trying to use slycot_horner …
sawyerbfuller Aug 17, 2020
4c3ed0b
renamed slycot_horner to slycot_laub
sawyerbfuller Aug 17, 2020
b37c16b
docs more consistent
sawyerbfuller Aug 17, 2020
4e4b97f
forgot to change a variable name everywhere in a function
sawyerbfuller Aug 17, 2020
5626a3a
revert doc/control.rst
bnavigator Aug 18, 2020
5caffa4
Update control/xferfcn.py
sawyerbfuller Aug 19, 2020
c888b6e
numpydoc convention updates
sawyerbfuller Aug 19, 2020
b64145d
small update to facilitate merge maybe
sawyerbfuller Aug 19, 2020
3cf80af
small numpydoc change in trasnferfunction to resolve merge
sawyerbfuller Aug 19, 2020
330e517
unit tests now test for deprecation of sys.freqresp
sawyerbfuller Aug 19, 2020
8c9571d
Merge branch 'master' into call-method
sawyerbfuller Aug 19, 2020
195bec3
rebase sawyerbfuller-call-method against master
murrayrm Jan 6, 2021
6b82c3c
Merge commit '195bec3' into call-method-mergeinto
bnavigator Jan 6, 2021
bddc792
fix merge strategy 'errors'
bnavigator Jan 6, 2021
e9bff6a
restore test_phase_crossover_frequencies_mimo
bnavigator Jan 6, 2021
73fc6a6
reinstate second check in frd_test test_eval()
bnavigator Jan 6, 2021
7acfc77
statesp_test: rename from test_evalfr to test_call
bnavigator Jan 6, 2021
a26520d
statesp_test: remove duplicate test_pole_static and test_copy_constru…
bnavigator Jan 6, 2021
dae8d42
Update control/xferfcn.py
sawyerbfuller Jan 6, 2021
3b10af0
check for warning the pytest way
bnavigator Jan 6, 2021
1bf445b
add unit tests for freqresp/evalfr warnings/errors
murrayrm Jan 7, 2021
3dbaacd
test frd freqresp deprecation
bnavigator Jan 7, 2021
178bfa0
doc updates to specify frequency response outputs more precisely
sawyerbfuller Jan 7, 2021
a631098
missed a couple of doc updates in xferfcn
sawyerbfuller Jan 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions control/bench/time_freqresp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
sys_tf = tf(sys)
w = logspace(-1,1,50)
ntimes = 1000
time_ss = timeit("sys.freqresp(w)", setup="from __main__ import sys, w", number=ntimes)
time_tf = timeit("sys_tf.freqresp(w)", setup="from __main__ import sys_tf, w", number=ntimes)
time_ss = timeit("sys.freqquency_response(w)", setup="from __main__ import sys, w", number=ntimes)
time_tf = timeit("sys_tf.frequency_response(w)", setup="from __main__ import sys_tf, w", number=ntimes)
print("State-space model on %d states: %f" % (nstates, time_ss))
print("Transfer-function model on %d states: %f" % (nstates, time_tf))
193 changes: 96 additions & 97 deletions control/frdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from warnings import warn
import numpy as np
from numpy import angle, array, empty, ones, \
real, imag, absolute, eye, linalg, where, dot
real, imag, absolute, eye, linalg, where, dot, sort
from scipy.interpolate import splprep, splev
from .lti import LTI

Expand Down Expand Up @@ -100,24 +100,18 @@ def __init__(self, *args, **kwargs):
object, other than an FRD, call FRD(sys, omega)

"""
# TODO: discrete-time FRD systems?
smooth = kwargs.get('smooth', False)

if len(args) == 2:
if not isinstance(args[0], FRD) and isinstance(args[0], LTI):
# not an FRD, but still a system, second argument should be
# the frequency range
otherlti = args[0]
self.omega = array(args[1], dtype=float)
self.omega.sort()
self.omega = sort(np.asarray(args[1], dtype=float))
numfreq = len(self.omega)

# calculate frequency response at my points
self.fresp = empty(
(otherlti.outputs, otherlti.inputs, numfreq),
dtype=complex)
for k, w in enumerate(self.omega):
self.fresp[:, :, k] = otherlti._evalfr(w)

self.fresp = otherlti(1j * self.omega, squeeze=False)
else:
# The user provided a response and a freq vector
self.fresp = array(args[0], dtype=complex)
Expand All @@ -141,7 +135,7 @@ def __init__(self, *args, **kwargs):
self.omega = args[0].omega
self.fresp = args[0].fresp
else:
raise ValueError("Needs 1 or 2 arguments; receivd %i." % len(args))
raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))

# create interpolation functions
if smooth:
Expand All @@ -163,17 +157,17 @@ def __str__(self):
mimo = self.inputs > 1 or self.outputs > 1
outstr = ['Frequency response data']

mt, pt, wt = self.freqresp(self.omega)
for i in range(self.inputs):
for j in range(self.outputs):
if mimo:
outstr.append("Input %i to output %i:" % (i + 1, j + 1))
outstr.append('Freq [rad/s] Response')
outstr.append('------------ ---------------------')
outstr.extend(
['%12.3f %10.4g%+10.4gj' % (w, m, p)
for m, p, w in zip(real(self.fresp[j, i, :]),
imag(self.fresp[j, i, :]), wt)])
['%12.3f %10.4g%+10.4gj' % (w, re, im)
for w, re, im in zip(self.omega,
real(self.fresp[j, i, :]),
imag(self.fresp[j, i, :]))])

return '\n'.join(outstr)

Expand Down Expand Up @@ -342,110 +336,116 @@ def __pow__(self, other):
return (FRD(ones(self.fresp.shape), self.omega) / self) * \
(self**(other+1))

def evalfr(self, omega):
"""Evaluate a transfer function at a single angular frequency.

self._evalfr(omega) returns the value of the frequency response
at frequency omega.

Note that a "normal" FRD only returns values for which there is an
entry in the omega vector. An interpolating FRD can return
intermediate values.

"""
warn("FRD.evalfr(omega) will be deprecated in a future release "
"of python-control; use sys.eval(omega) instead",
PendingDeprecationWarning) # pragma: no coverage
return self._evalfr(omega)

# Define the `eval` function to evaluate an FRD at a given (real)
# frequency. Note that we choose to use `eval` instead of `evalfr` to
# avoid confusion with :func:`evalfr`, which takes a complex number as its
# argument. Similarly, we don't use `__call__` to avoid confusion between
# G(s) for a transfer function and G(omega) for an FRD object.
def eval(self, omega):
"""Evaluate a transfer function at a single angular frequency.

self.evalfr(omega) returns the value of the frequency response
at frequency omega.
# update Sawyer B. Fuller 2020.08.14: __call__ added to provide a uniform
# interface to systems in general and the lti.frequency_response method
def eval(self, omega, squeeze=True):
"""Evaluate a transfer function at angular frequency omega.

Note that a "normal" FRD only returns values for which there is an
entry in the omega vector. An interpolating FRD can return
intermediate values.

Parameters
----------
omega : float or array_like
Frequencies in radians per second
squeeze : bool, optional (default=True)
If True and `sys` is single input single output (SISO), returns a
1D array rather than a 3D array.

Returns
-------
fresp : (self.outputs, self.inputs, len(x)) or (len(x), ) complex ndarray
The frequency response of the system. Array is ``len(x)`` if and only
if system is SISO and ``squeeze=True``.
"""
return self._evalfr(omega)

# Internal function to evaluate the frequency responses
def _evalfr(self, omega):
"""Evaluate a transfer function at a single angular frequency."""
# Preallocate the output.
if getattr(omega, '__iter__', False):
out = empty((self.outputs, self.inputs, len(omega)), dtype=complex)
else:
out = empty((self.outputs, self.inputs), dtype=complex)
omega_array = np.array(omega, ndmin=1) # array-like version of omega
if any(omega_array.imag > 0):
raise ValueError("FRD.eval can only accept real-valued omega")

if self.ifunc is None:
try:
out = self.fresp[:, :, where(self.omega == omega)[0][0]]
except Exception:
elements = np.isin(self.omega, omega) # binary array
if sum(elements) < len(omega_array):
raise ValueError(
"Frequency %f not in frequency list, try an interpolating"
" FRD if you want additional points" % omega)
else:
if getattr(omega, '__iter__', False):
for i in range(self.outputs):
for j in range(self.inputs):
for k, w in enumerate(omega):
frraw = splev(w, self.ifunc[i, j], der=0)
out[i, j, k] = frraw[0] + 1.0j * frraw[1]
"not all frequencies omega are in frequency list of FRD "
"system. Try an interpolating FRD for additional points.")
else:
for i in range(self.outputs):
for j in range(self.inputs):
frraw = splev(omega, self.ifunc[i, j], der=0)
out[i, j] = frraw[0] + 1.0j * frraw[1]

out = self.fresp[:, :, elements]
else:
out = empty((self.outputs, self.inputs, len(omega_array)),
dtype=complex)
for i in range(self.outputs):
for j in range(self.inputs):
for k, w in enumerate(omega_array):
frraw = splev(w, self.ifunc[i, j], der=0)
out[i, j, k] = frraw[0] + 1.0j * frraw[1]
if not hasattr(omega, '__len__'):
# omega is a scalar, squeeze down array along last dim
out = np.squeeze(out, axis=2)
if squeeze and self.issiso():
out = out[0][0]
return out

# Method for generating the frequency response of the system
def freqresp(self, omega):
"""Evaluate the frequency response at a list of angular frequencies.
def __call__(self, s, squeeze=True):
"""Evaluate system's transfer function at complex frequencies.

Returns the complex frequency response `sys(s)` of system `sys` with
`m = sys.inputs` number of inputs and `p = sys.outputs` number of
outputs.

Reports the value of the magnitude, phase, and angular frequency of
the requency response evaluated at omega, where omega is a list of
angular frequencies, and is a sorted version of the input omega.
To evaluate at a frequency omega in radians per second, enter
``s = omega * 1j`` or use ``sys.eval(omega)``

Parameters
----------
omega : array_like
A list of frequencies in radians/sec at which the system should be
evaluated. The list can be either a python list or a numpy array
and will be sorted before evaluation.
s : complex scalar or array_like
Complex frequencies
squeeze : bool, optional (default=True)
If True and `sys` is single input single output (SISO), i.e. `m=1`,
`p=1`, return a 1D array rather than a 3D array.

Returns
-------
mag : (self.outputs, self.inputs, len(omega)) ndarray
The magnitude (absolute value, not dB or log10) of the system
frequency response.
phase : (self.outputs, self.inputs, len(omega)) ndarray
The wrapped phase in radians of the system frequency response.
omega : ndarray or list or tuple
The list of sorted frequencies at which the response was
evaluated.
"""
# Preallocate outputs.
numfreq = len(omega)
mag = empty((self.outputs, self.inputs, numfreq))
phase = empty((self.outputs, self.inputs, numfreq))
fresp : (p, m, len(s)) complex ndarray or (len(s),) complex ndarray
The frequency response of the system. Array is ``(len(s), )`` if
and only if system is SISO and ``squeeze=True``.

omega.sort()

for k, w in enumerate(omega):
fresp = self._evalfr(w)
mag[:, :, k] = abs(fresp)
phase[:, :, k] = angle(fresp)
Raises
------
ValueError
If `s` is not purely imaginary, because
:class:`FrequencyDomainData` systems are only defined at imaginary
frequency values.
"""
if any(abs(np.array(s, ndmin=1).real) > 0):
raise ValueError("__call__: FRD systems can only accept "
"purely imaginary frequencies")
# need to preserve array or scalar status
if hasattr(s, '__len__'):
return self.eval(np.asarray(s).imag, squeeze=squeeze)
else:
return self.eval(complex(s).imag, squeeze=squeeze)

return mag, phase, omega
def freqresp(self, omega):
"""(deprecated) Evaluate transfer function at complex frequencies.

.. deprecated::0.9.0
Method has been given the more pythonic name
:meth:`FrequencyResponseData.frequency_response`. Or use
:func:`freqresp` in the MATLAB compatibility module.
"""
warn("FrequencyResponseData.freqresp(omega) will be removed in a "
"future release of python-control; use "
"FrequencyResponseData.frequency_response(omega), or "
"freqresp(sys, omega) in the MATLAB compatibility module "
"instead", DeprecationWarning)
return self.frequency_response(omega)

def feedback(self, other=1, sign=-1):
"""Feedback interconnection between two FRD objects."""
Expand Down Expand Up @@ -515,11 +515,10 @@ def _convertToFRD(sys, omega, inputs=1, outputs=1):
"Frequency ranges of FRD do not match, conversion not implemented")

elif isinstance(sys, LTI):
omega.sort()
fresp = empty((sys.outputs, sys.inputs, len(omega)), dtype=complex)
for k, w in enumerate(omega):
fresp[:, :, k] = sys._evalfr(w)

omega = np.sort(omega)
fresp = sys(1j * omega)
if len(fresp.shape) == 1:
fresp = fresp[np.newaxis, np.newaxis, :]
return FRD(fresp, omega, smooth=True)

elif isinstance(sys, (int, float, complex, np.number)):
Expand Down
24 changes: 12 additions & 12 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ def bode_plot(syslist, omega=None,

Notes
-----
1. Alternatively, you may use the lower-level method
``(mag, phase, freq) = sys.freqresp(freq)`` to generate the frequency
response for a system, but it returns a MIMO response.
1. Alternatively, you may use the lower-level methods
:meth:`LTI.frequency_response` or ``sys(s)`` or ``sys(z)`` or to
generate the frequency response for a single system.

2. If a discrete time model is given, the frequency response is plotted
along the upper branch of the unit circle, using the mapping z = exp(j
\\omega dt) where omega ranges from 0 to pi/dt and dt is the discrete
timebase. If not timebase is specified (dt = True), dt is set to 1.
along the upper branch of the unit circle, using the mapping ``z = exp(1j
* omega * dt)`` where `omega` ranges from 0 to `pi/dt` and `dt` is the discrete
timebase. If timebase not specified (``dt=True``), `dt` is set to 1.

Examples
--------
Expand Down Expand Up @@ -228,7 +228,7 @@ def bode_plot(syslist, omega=None,
nyquistfrq = None

# Get the magnitude and phase of the system
mag_tmp, phase_tmp, omega_sys = sys.freqresp(omega_sys)
mag_tmp, phase_tmp, omega_sys = sys.frequency_response(omega_sys)
mag = np.atleast_1d(np.squeeze(mag_tmp))
phase = np.atleast_1d(np.squeeze(phase_tmp))

Expand Down Expand Up @@ -588,7 +588,7 @@ def nyquist_plot(syslist, omega=None, plot=True, label_freq=0,
"Nyquist is currently only implemented for SISO systems.")
else:
# Get the magnitude and phase of the system
mag_tmp, phase_tmp, omega = sys.freqresp(omega)
mag_tmp, phase_tmp, omega = sys.frequency_response(omega)
mag = np.squeeze(mag_tmp)
phase = np.squeeze(phase_tmp)

Expand Down Expand Up @@ -718,7 +718,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
omega_plot = omega / (2. * math.pi) if Hz else omega

# TODO: Need to add in the mag = 1 lines
mag_tmp, phase_tmp, omega = S.freqresp(omega)
mag_tmp, phase_tmp, omega = S.frequency_response(omega)
mag = np.squeeze(mag_tmp)
if dB:
plot_axes['s'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs)
Expand All @@ -728,7 +728,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
plot_axes['s'].tick_params(labelbottom=False)
plot_axes['s'].grid(grid, which='both')

mag_tmp, phase_tmp, omega = (P * S).freqresp(omega)
mag_tmp, phase_tmp, omega = (P * S).frequency_response(omega)
mag = np.squeeze(mag_tmp)
if dB:
plot_axes['ps'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs)
Expand All @@ -738,7 +738,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
plot_axes['ps'].set_ylabel("$|PS|$" + " (dB)" if dB else "")
plot_axes['ps'].grid(grid, which='both')

mag_tmp, phase_tmp, omega = (C * S).freqresp(omega)
mag_tmp, phase_tmp, omega = (C * S).frequency_response(omega)
mag = np.squeeze(mag_tmp)
if dB:
plot_axes['cs'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs)
Expand All @@ -749,7 +749,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
plot_axes['cs'].set_ylabel("$|CS|$" + " (dB)" if dB else "")
plot_axes['cs'].grid(grid, which='both')

mag_tmp, phase_tmp, omega = T.freqresp(omega)
mag_tmp, phase_tmp, omega = T.frequency_response(omega)
mag = np.squeeze(mag_tmp)
if dB:
plot_axes['t'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs)
Expand Down
Loading