10
10
FRD data.
11
11
"""
12
12
13
+ from collections .abc import Iterable
13
14
from copy import copy
14
15
from warnings import warn
15
16
20
21
21
22
from . import config
22
23
from .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
24
26
from .lti import LTI , _process_frequency_response
25
27
26
28
__all__ = ['FrequencyResponseData' , 'FRD' , 'frd' ]
@@ -33,8 +35,8 @@ class FrequencyResponseData(LTI):
33
35
34
36
The FrequencyResponseData (FRD) class is used to represent systems in
35
37
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.
38
40
39
41
Parameters
40
42
----------
@@ -65,6 +67,28 @@ class constructor, using the :func:~~control.frd` factory function
65
67
frequency point.
66
68
dt : float, True, or None
67
69
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.
68
92
69
93
See Also
70
94
--------
@@ -89,6 +113,20 @@ class constructor, using the :func:~~control.frd` factory function
89
113
the imaginary access). See :meth:`~control.FrequencyResponseData.__call__`
90
114
for a more detailed description.
91
115
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
+
92
130
"""
93
131
#
94
132
# Class attributes
@@ -243,21 +281,72 @@ def __init__(self, *args, **kwargs):
243
281
244
282
@property
245
283
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 )
247
300
248
301
@property
249
302
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 )
251
319
252
320
@property
253
321
def frequency (self ):
322
+ """Frequencies at which the response is evaluated.
323
+
324
+ :type: 1D array
325
+
326
+ """
254
327
return self .omega
255
328
256
329
@property
257
330
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 )
259
347
260
348
def __str__ (self ):
349
+
261
350
"""String representation of the transfer function."""
262
351
263
352
mimo = self .ninputs > 1 or self .noutputs > 1
@@ -593,9 +682,25 @@ def __iter__(self):
593
682
return iter ((self .omega , fresp ))
594
683
return iter ((np .abs (fresp ), np .angle (fresp ), self .omega ))
595
684
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 )
599
704
600
705
# Implement (thin) len to emulate legacy testing interface
601
706
def __len__ (self ):
0 commit comments