Skip to content

Commit 6c3c630

Browse files
committed
frequency_response() returns FRD; FRD allows return_magphase
1 parent 51f6bfc commit 6c3c630

File tree

5 files changed

+91
-30
lines changed

5 files changed

+91
-30
lines changed

control/frdata.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@
4444
"""
4545

4646
# External function declarations
47+
from copy import copy
4748
from warnings import warn
49+
4850
import numpy as np
4951
from numpy import angle, array, empty, ones, \
5052
real, imag, absolute, eye, linalg, where, sort
5153
from scipy.interpolate import splprep, splev
54+
5255
from .lti import LTI, _process_frequency_response
5356
from .exception import pandas_check
5457
from .namedio import _NamedIOSystem
@@ -141,7 +144,7 @@ def __init__(self, *args, **kwargs):
141144
The default constructor is FRD(d, w), where w is an iterable of
142145
frequency points, and d is the matching frequency data.
143146
144-
If d is a single list, 1d array, or tuple, a SISO system description
147+
If d is a single list, 1D array, or tuple, a SISO system description
145148
is assumed. d can also be
146149
147150
To call the copy constructor, call FRD(sys), where sys is a
@@ -170,13 +173,12 @@ def __init__(self, *args, **kwargs):
170173

171174
else:
172175
# The user provided a response and a freq vector
173-
self.fresp = array(args[0], dtype=complex)
174-
if len(self.fresp.shape) == 1:
175-
self.fresp = self.fresp.reshape(1, 1, len(args[0]))
176-
self.omega = array(args[1], dtype=float)
177-
if len(self.fresp.shape) != 3 or \
178-
self.fresp.shape[-1] != self.omega.shape[-1] or \
179-
len(self.omega.shape) != 1:
176+
self.fresp = array(args[0], dtype=complex, ndmin=1)
177+
if self.fresp.ndim == 1:
178+
self.fresp = self.fresp.reshape(1, 1, -1)
179+
self.omega = array(args[1], dtype=float, ndmin=1)
180+
if self.fresp.ndim != 3 or self.omega.ndim != 1 or \
181+
self.fresp.shape[-1] != self.omega.shape[-1]:
180182
raise TypeError(
181183
"The frequency data constructor needs a 1-d or 3-d"
182184
" response data array and a matching frequency vector"
@@ -206,6 +208,12 @@ def __init__(self, *args, **kwargs):
206208

207209
# Keep track of return type
208210
self.return_magphase=kwargs.pop('return_magphase', False)
211+
if self.return_magphase not in (True, False):
212+
raise ValueError("unknown return_magphase value")
213+
214+
self.squeeze=kwargs.pop('squeeze', None)
215+
if self.squeeze not in (None, True, False):
216+
raise ValueError("unknown squeeze value")
209217

210218
# Make sure there were no extraneous keywords
211219
if kwargs:
@@ -477,7 +485,7 @@ def eval(self, omega, squeeze=None):
477485

478486
return _process_frequency_response(self, omega, out, squeeze=squeeze)
479487

480-
def __call__(self, s, squeeze=None):
488+
def __call__(self, s=None, squeeze=None, return_magphase=None):
481489
"""Evaluate system's transfer function at complex frequencies.
482490
483491
Returns the complex frequency response `sys(s)` of system `sys` with
@@ -490,17 +498,31 @@ def __call__(self, s, squeeze=None):
490498
For a frequency response data object, the argument must be an
491499
imaginary number (since only the frequency response is defined).
492500
501+
If ``s`` is not given, this function creates a copy of a frequency
502+
response data object with a different set of output settings.
503+
493504
Parameters
494505
----------
495506
s : complex scalar or 1D array_like
496-
Complex frequencies
497-
squeeze : bool, optional (default=True)
507+
Complex frequencies. If not specified, return a copy of the
508+
frequency response data object with updated settings for output
509+
processing (``squeeze``, ``return_magphase``).
510+
511+
squeeze : bool, optional
498512
If squeeze=True, remove single-dimensional entries from the shape
499513
of the output even if the system is not SISO. If squeeze=False,
500514
keep all indices (output, input and, if omega is array_like,
501515
frequency) even if the system is SISO. The default value can be
502516
set using config.defaults['control.squeeze_frequency_response'].
503517
518+
return_magphase : bool, optional
519+
If True, then a frequency response data object will enumerate as a
520+
tuple of the form (mag, phase, omega) where where ``mag`` is the
521+
magnitude (absolute value, not dB or log10) of the system
522+
frequency response, ``phase`` is the wrapped phase in radians of
523+
the system frequency response, and ``omega`` is the (sorted)
524+
frequencies at which the response was evaluated.
525+
504526
Returns
505527
-------
506528
fresp : complex ndarray
@@ -519,6 +541,17 @@ def __call__(self, s, squeeze=None):
519541
frequency values.
520542
521543
"""
544+
if s is None:
545+
# Create a copy of the response with new keywords
546+
response = copy(self)
547+
548+
# Update any keywords that we were passed
549+
response.squeeze = self.squeeze if squeeze is None else squeeze
550+
response.return_magphase = self.return_magphase \
551+
if return_magphase is None else return_magphase
552+
553+
return response
554+
522555
# Make sure that we are operating on a simple list
523556
if len(np.atleast_1d(s).shape) > 1:
524557
raise ValueError("input list must be 1D")
@@ -533,6 +566,22 @@ def __call__(self, s, squeeze=None):
533566
else:
534567
return self.eval(complex(s).imag, squeeze=squeeze)
535568

569+
# Implement iter to allow assigning to a tuple
570+
def __iter__(self):
571+
fresp = _process_frequency_response(
572+
self, self.omega, self.fresp, squeeze=self.squeeze)
573+
if not self.return_magphase:
574+
return iter((self.omega, fresp))
575+
return iter((np.abs(fresp), np.angle(fresp), self.omega))
576+
577+
# Implement (thin) getitem to allow access via legacy indexing
578+
def __getitem__(self, index):
579+
return list(self.__iter__())[index]
580+
581+
# Implement (thin) len to emulate legacy testing interface
582+
def __len__(self):
583+
return 3 if self.return_magphase else 2
584+
536585
def freqresp(self, omega):
537586
"""(deprecated) Evaluate transfer function at complex frequencies.
538587

control/lti.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def _set_inputs(self, value):
7777
"""
7878
Deprecated attribute; use :attr:`ninputs` instead.
7979
80-
The ``input`` attribute was used to store the number of system inputs.
81-
It is no longer used. If you need access to the number of inputs for
82-
an LTI system, use :attr:`ninputs`.
80+
The ``inputs`` attribute was used to store the number of system
81+
inputs. It is no longer used. If you need access to the number
82+
of inputs for an LTI system, use :attr:`ninputs`.
8383
""")
8484

8585
def _get_outputs(self):
@@ -100,7 +100,7 @@ def _set_outputs(self, value):
100100
"""
101101
Deprecated attribute; use :attr:`noutputs` instead.
102102
103-
The ``output`` attribute was used to store the number of system
103+
The ``outputs`` attribute was used to store the number of system
104104
outputs. It is no longer used. If you need access to the number of
105105
outputs for an LTI system, use :attr:`noutputs`.
106106
""")
@@ -197,17 +197,21 @@ def frequency_response(self, omega, squeeze=None):
197197
198198
Returns
199199
-------
200-
mag : ndarray
201-
The magnitude (absolute value, not dB or log10) of the system
202-
frequency response. If the system is SISO and squeeze is not
203-
True, the array is 1D, indexed by frequency. If the system is not
204-
SISO or squeeze is False, the array is 3D, indexed by the output,
205-
input, and frequency. If ``squeeze`` is True then
206-
single-dimensional axes are removed.
207-
phase : ndarray
208-
The wrapped phase in radians of the system frequency response.
209-
omega : ndarray
210-
The (sorted) frequencies at which the response was evaluated.
200+
response : :class:`FrequencyReponseData`
201+
Frequency response data object representing the frequency
202+
response. This object can be assigned to a tuple using
203+
204+
mag, phase, omega = response
205+
206+
where ``mag`` is the magnitude (absolute value, not dB or log10)
207+
of the system frequency response, ``phase`` is the wrapped phase
208+
in radians of the system frequency response, and ``omega`` is the
209+
(sorted) frequencies at which the response was evaluated. If the
210+
system is SISO and squeeze is not True, ``mag`` and ``phase`` are
211+
1D, indexed by frequency. If the system is not SISO or squeeze is
212+
False, the array is 3D, indexed by the output, input, and
213+
frequency. If ``squeeze`` is True then single-dimensional axes
214+
are removed.
211215
212216
"""
213217
omega = np.sort(np.array(omega, ndmin=1))
@@ -218,8 +222,12 @@ def frequency_response(self, omega, squeeze=None):
218222
s = np.exp(1j * omega * self.dt)
219223
else:
220224
s = 1j * omega
221-
response = self.__call__(s, squeeze=squeeze)
222-
return abs(response), angle(response), omega
225+
226+
# Return the data as a frequency response data object
227+
from .frdata import FrequencyResponseData
228+
response = self.__call__(s)
229+
return FrequencyResponseData(
230+
response, omega, return_magphase=True, squeeze=squeeze)
223231

224232
def dcgain(self):
225233
"""Return the zero-frequency gain"""

control/tests/lti_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def test_squeeze_exceptions(self, fcn):
267267
sys = fcn(ct.rss(2, 1, 1))
268268

269269
with pytest.raises(ValueError, match="unknown squeeze value"):
270-
sys.frequency_response([1], squeeze=1)
270+
resp = sys.frequency_response([1], squeeze='siso')
271271
with pytest.raises(ValueError, match="unknown squeeze value"):
272272
sys([1j], squeeze='siso')
273273
with pytest.raises(ValueError, match="unknown squeeze value"):

control/timeresp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def __call__(self, **kwargs):
463463
# Update any keywords that we were passed
464464
response.transpose = kwargs.pop('transpose', self.transpose)
465465
response.squeeze = kwargs.pop('squeeze', self.squeeze)
466-
response.return_x = kwargs.pop('return_x', self.squeeze)
466+
response.return_x = kwargs.pop('return_x', self.return_x)
467467

468468
# Check for new labels
469469
input_labels = kwargs.pop('input_labels', None)

control/xferfcn.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from re import sub
6262
from .lti import LTI, common_timebase, isdtime, _process_frequency_response
6363
from .exception import ControlMIMONotImplemented
64+
from .frdata import FrequencyResponseData
6465
from .namedio import _NamedIOSystem, _process_signal_list
6566
from . import config
6667

@@ -1382,6 +1383,9 @@ def _convert_to_transfer_function(sys, inputs=1, outputs=1):
13821383

13831384
return TransferFunction(num, den)
13841385

1386+
elif isinstance(sys, FrequencyResponseData):
1387+
raise TypeError("Can't convert given FRD to TransferFunction system.")
1388+
13851389
# If this is array-like, try to create a constant feedthrough
13861390
try:
13871391
D = array(sys, ndmin=2)

0 commit comments

Comments
 (0)