1010FRD data.
1111"""
1212
13+ from collections .abc import Iterable
1314from copy import copy
1415from warnings import warn
1516
2021
2122from . import config
2223from .exception import pandas_check
23- from .iosys import InputOutputSystem , _process_iosys_keywords , common_timebase
24+ from .iosys import InputOutputSystem , NamedSignal , _process_iosys_keywords , \
25+ _process_subsys_index , common_timebase
2426from .lti import LTI , _process_frequency_response
2527
2628__all__ = ['FrequencyResponseData' , 'FRD' , 'frd' ]
@@ -33,8 +35,8 @@ class FrequencyResponseData(LTI):
3335
3436 The FrequencyResponseData (FRD) class is used to represent systems in
3537 frequency response data form. It can be created manually using the
36- class constructor, using the :func:~~ control.frd` factory function
37- (preferred), or via the :func:`~control.frequency_response` function.
38+ class constructor, using the :func:`~ control.frd` factory function or
39+ via the :func:`~control.frequency_response` function.
3840
3941 Parameters
4042 ----------
@@ -65,6 +67,28 @@ class constructor, using the :func:~~control.frd` factory function
6567 frequency point.
6668 dt : float, True, or None
6769 System timebase.
70+ squeeze : bool
71+ By default, if a system is single-input, single-output (SISO) then
72+ the outputs (and inputs) are returned as a 1D array (indexed by
73+ frequency) and if a system is multi-input or multi-output, then the
74+ outputs are returned as a 2D array (indexed by output and
75+ frequency) or a 3D array (indexed by output, trace, and frequency).
76+ If ``squeeze=True``, access to the output response will remove
77+ single-dimensional entries from the shape of the inputs and outputs
78+ even if the system is not SISO. If ``squeeze=False``, the output is
79+ returned as a 3D array (indexed by the output, input, and
80+ frequency) even if the system is SISO. The default value can be set
81+ using config.defaults['control.squeeze_frequency_response'].
82+ ninputs, noutputs, nstates : int
83+ Number of inputs, outputs, and states of the underlying system.
84+ input_labels, output_labels : array of str
85+ Names for the input and output variables.
86+ sysname : str, optional
87+ Name of the system. For data generated using
88+ :func:`~control.frequency_response`, stores the name of the system
89+ that created the data.
90+ title : str, optional
91+ Set the title to use when plotting.
6892
6993 See Also
7094 --------
@@ -89,6 +113,20 @@ class constructor, using the :func:~~control.frd` factory function
89113 the imaginary access). See :meth:`~control.FrequencyResponseData.__call__`
90114 for a more detailed description.
91115
116+ A state space system is callable and returns the value of the transfer
117+ function evaluated at a point in the complex plane. See
118+ :meth:`~control.StateSpace.__call__` for a more detailed description.
119+
120+ Subsystem response corresponding to selected input/output pairs can be
121+ created by indexing the frequency response data object::
122+
123+ subsys = sys[output_spec, input_spec]
124+
125+ The input and output specifications can be single integers, lists of
126+ integers, or slices. In addition, the strings representing the names
127+ of the signals can be used and will be replaced with the equivalent
128+ signal offsets.
129+
92130 """
93131 #
94132 # Class attributes
@@ -223,14 +261,20 @@ def __init__(self, *args, **kwargs):
223261
224262 # create interpolation functions
225263 if smooth :
264+ # Set the order of the fit
265+ if self .omega .size < 2 :
266+ raise ValueError ("can't smooth with only 1 frequency" )
267+ degree = 3 if self .omega .size > 3 else self .omega .size - 1
268+
226269 self .ifunc = empty ((self .fresp .shape [0 ], self .fresp .shape [1 ]),
227270 dtype = tuple )
228271 for i in range (self .fresp .shape [0 ]):
229272 for j in range (self .fresp .shape [1 ]):
230273 self .ifunc [i , j ], u = splprep (
231274 u = self .omega , x = [real (self .fresp [i , j , :]),
232275 imag (self .fresp [i , j , :])],
233- w = 1.0 / (absolute (self .fresp [i , j , :]) + 0.001 ), s = 0.0 )
276+ w = 1.0 / (absolute (self .fresp [i , j , :]) + 0.001 ),
277+ s = 0.0 , k = degree )
234278 else :
235279 self .ifunc = None
236280
@@ -243,21 +287,72 @@ def __init__(self, *args, **kwargs):
243287
244288 @property
245289 def magnitude (self ):
246- return np .abs (self .fresp )
290+ """Magnitude of the frequency response.
291+
292+ Magnitude of the frequency response, indexed by either the output
293+ and frequency (if only a single input is given) or the output,
294+ input, and frequency (for multi-input systems). See
295+ :attr:`FrequencyResponseData.squeeze` for a description of how this
296+ can be modified using the `squeeze` keyword.
297+
298+ Input and output signal names can be used to index the data in
299+ place of integer offsets.
300+
301+ :type: 1D, 2D, or 3D array
302+
303+ """
304+ return NamedSignal (
305+ np .abs (self .fresp ), self .output_labels , self .input_labels )
247306
248307 @property
249308 def phase (self ):
250- return np .angle (self .fresp )
309+ """Phase of the frequency response.
310+
311+ Phase of the frequency response in radians/sec, indexed by either
312+ the output and frequency (if only a single input is given) or the
313+ output, input, and frequency (for multi-input systems). See
314+ :attr:`FrequencyResponseData.squeeze` for a description of how this
315+ can be modified using the `squeeze` keyword.
316+
317+ Input and output signal names can be used to index the data in
318+ place of integer offsets.
319+
320+ :type: 1D, 2D, or 3D array
321+
322+ """
323+ return NamedSignal (
324+ np .angle (self .fresp ), self .output_labels , self .input_labels )
251325
252326 @property
253327 def frequency (self ):
328+ """Frequencies at which the response is evaluated.
329+
330+ :type: 1D array
331+
332+ """
254333 return self .omega
255334
256335 @property
257336 def response (self ):
258- return self .fresp
337+ """Complex value of the frequency response.
338+
339+ Value of the frequency response as a complex number, indexed by
340+ either the output and frequency (if only a single input is given)
341+ or the output, input, and frequency (for multi-input systems). See
342+ :attr:`FrequencyResponseData.squeeze` for a description of how this
343+ can be modified using the `squeeze` keyword.
344+
345+ Input and output signal names can be used to index the data in
346+ place of integer offsets.
347+
348+ :type: 1D, 2D, or 3D array
349+
350+ """
351+ return NamedSignal (
352+ self .fresp , self .output_labels , self .input_labels )
259353
260354 def __str__ (self ):
355+
261356 """String representation of the transfer function."""
262357
263358 mimo = self .ninputs > 1 or self .noutputs > 1
@@ -303,7 +398,12 @@ def __add__(self, other):
303398
304399 # Convert the second argument to a frequency response function.
305400 # or re-base the frd to the current omega (if needed)
306- other = _convert_to_frd (other , omega = self .omega )
401+ if isinstance (other , (int , float , complex , np .number )):
402+ other = _convert_to_frd (
403+ other , omega = self .omega ,
404+ inputs = self .ninputs , outputs = self .noutputs )
405+ else :
406+ other = _convert_to_frd (other , omega = self .omega )
307407
308408 # Check that the input-output sizes are consistent.
309409 if self .ninputs != other .ninputs :
@@ -593,9 +693,25 @@ def __iter__(self):
593693 return iter ((self .omega , fresp ))
594694 return iter ((np .abs (fresp ), np .angle (fresp ), self .omega ))
595695
596- # Implement (thin) getitem to allow access via legacy indexing
597- def __getitem__ (self , index ):
598- return list (self .__iter__ ())[index ]
696+ def __getitem__ (self , key ):
697+ if not isinstance (key , Iterable ) or len (key ) != 2 :
698+ # Implement (thin) getitem to allow access via legacy indexing
699+ return list (self .__iter__ ())[key ]
700+
701+ # Convert signal names to integer offsets (via NamedSignal object)
702+ iomap = NamedSignal (
703+ self .fresp [:, :, 0 ], self .output_labels , self .input_labels )
704+ indices = iomap ._parse_key (key , level = 1 ) # ignore index checks
705+ outdx , outputs = _process_subsys_index (indices [0 ], self .output_labels )
706+ inpdx , inputs = _process_subsys_index (indices [1 ], self .input_labels )
707+
708+ # Create the system name
709+ sysname = config .defaults ['iosys.indexed_system_name_prefix' ] + \
710+ self .name + config .defaults ['iosys.indexed_system_name_suffix' ]
711+
712+ return FrequencyResponseData (
713+ self .fresp [outdx , :][:, inpdx ], self .omega , self .dt ,
714+ inputs = inputs , outputs = outputs , name = sysname )
599715
600716 # Implement (thin) len to emulate legacy testing interface
601717 def __len__ (self ):
0 commit comments