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
@@ -243,21 +281,72 @@ def __init__(self, *args, **kwargs):
243281
244282 @property
245283 def magnitude (self ):
246- return np .abs (self .fresp )
284+ """Magnitude of the frequency response.
285+
286+ Magnitude of the frequency response, indexed by either the output
287+ and frequency (if only a single input is given) or the output,
288+ input, and frequency (for multi-input systems). See
289+ :attr:`FrequencyResponseData.squeeze` for a description of how this
290+ can be modified using the `squeeze` keyword.
291+
292+ Input and output signal names can be used to index the data in
293+ place of integer offsets.
294+
295+ :type: 1D, 2D, or 3D array
296+
297+ """
298+ return NamedSignal (
299+ np .abs (self .fresp ), self .output_labels , self .input_labels )
247300
248301 @property
249302 def phase (self ):
250- return np .angle (self .fresp )
303+ """Phase of the frequency response.
304+
305+ Phase of the frequency response in radians/sec, indexed by either
306+ the output and frequency (if only a single input is given) or the
307+ output, input, and frequency (for multi-input systems). See
308+ :attr:`FrequencyResponseData.squeeze` for a description of how this
309+ can be modified using the `squeeze` keyword.
310+
311+ Input and output signal names can be used to index the data in
312+ place of integer offsets.
313+
314+ :type: 1D, 2D, or 3D array
315+
316+ """
317+ return NamedSignal (
318+ np .angle (self .fresp ), self .output_labels , self .input_labels )
251319
252320 @property
253321 def frequency (self ):
322+ """Frequencies at which the response is evaluated.
323+
324+ :type: 1D array
325+
326+ """
254327 return self .omega
255328
256329 @property
257330 def response (self ):
258- return self .fresp
331+ """Complex value of the frequency response.
332+
333+ Value of the frequency response as a complex number, indexed by
334+ either the output and frequency (if only a single input is given)
335+ or the output, input, and frequency (for multi-input systems). See
336+ :attr:`FrequencyResponseData.squeeze` for a description of how this
337+ can be modified using the `squeeze` keyword.
338+
339+ Input and output signal names can be used to index the data in
340+ place of integer offsets.
341+
342+ :type: 1D, 2D, or 3D array
343+
344+ """
345+ return NamedSignal (
346+ self .fresp , self .output_labels , self .input_labels )
259347
260348 def __str__ (self ):
349+
261350 """String representation of the transfer function."""
262351
263352 mimo = self .ninputs > 1 or self .noutputs > 1
@@ -593,9 +682,25 @@ def __iter__(self):
593682 return iter ((self .omega , fresp ))
594683 return iter ((np .abs (fresp ), np .angle (fresp ), self .omega ))
595684
596- # Implement (thin) getitem to allow access via legacy indexing
597- def __getitem__ (self , index ):
598- return list (self .__iter__ ())[index ]
685+ def __getitem__ (self , key ):
686+ if not isinstance (key , Iterable ) or len (key ) != 2 :
687+ # Implement (thin) getitem to allow access via legacy indexing
688+ return list (self .__iter__ ())[key ]
689+
690+ # Convert signal names to integer offsets (via NamedSignal object)
691+ iomap = NamedSignal (
692+ self .fresp [:, :, 0 ], self .output_labels , self .input_labels )
693+ indices = iomap ._parse_key (key , level = 1 ) # ignore index checks
694+ outdx , outputs = _process_subsys_index (indices [0 ], self .output_labels )
695+ inpdx , inputs = _process_subsys_index (indices [1 ], self .input_labels )
696+
697+ # Create the system name
698+ sysname = config .defaults ['iosys.indexed_system_name_prefix' ] + \
699+ self .name + config .defaults ['iosys.indexed_system_name_suffix' ]
700+
701+ return FrequencyResponseData (
702+ self .fresp [outdx , :][:, inpdx ], self .omega , self .dt ,
703+ inputs = inputs , outputs = outputs , name = sysname )
599704
600705 # Implement (thin) len to emulate legacy testing interface
601706 def __len__ (self ):
0 commit comments