Merge pull request #1069 from murrayrm/named_signals-29Nov2024 · python-control/python-control@12dda4e
@@ -10,6 +10,7 @@
1010FRD data.
1111"""
121213+from collections.abc import Iterable
1314from copy import copy
1415from warnings import warn
1516@@ -20,7 +21,8 @@
20212122from . 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
25272628__all__ = ['FrequencyResponseData', 'FRD', 'frd']
@@ -33,8 +35,8 @@ class FrequencyResponseData(LTI):
33353436 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.
38403941 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.
68926993 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.
91115116+ 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):
243281244282@property
245283def 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)
247300248301@property
249302def 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)
251319252320@property
253321def frequency(self):
322+"""Frequencies at which the response is evaluated.
323+324+ :type: 1D array
325+326+ """
254327return self.omega
255328256329@property
257330def 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)
259347260348def __str__(self):
349+261350"""String representation of the transfer function."""
262351263352mimo = self.ninputs > 1 or self.noutputs > 1
@@ -593,9 +682,25 @@ def __iter__(self):
593682return iter((self.omega, fresp))
594683return iter((np.abs(fresp), np.angle(fresp), self.omega))
595684596-# 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)
599704600705# Implement (thin) len to emulate legacy testing interface
601706def __len__(self):