4444"""
4545
4646# External function declarations
47+ from copy import copy
4748from warnings import warn
49+
4850import numpy as np
4951from numpy import angle , array , empty , ones , \
5052 real , imag , absolute , eye , linalg , where , sort
5153from scipy .interpolate import splprep , splev
54+
5255from .lti import LTI , _process_frequency_response
5356from .exception import pandas_check
5457from .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
0 commit comments