Merge pull request #1069 from murrayrm/named_signals-29Nov2024 · python-control/python-control@12dda4e

@@ -10,6 +10,7 @@

1010

FRD data.

1111

"""

121213+

from collections.abc import Iterable

1314

from copy import copy

1415

from warnings import warn

1516

@@ -20,7 +21,8 @@

20212122

from . import config

2223

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

2426

from .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

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)

247300248301

@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)

251319252320

@property

253321

def frequency(self):

322+

"""Frequencies at which the response is evaluated.

323+324+

:type: 1D array

325+326+

"""

254327

return self.omega

255328256329

@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)

259347260348

def __str__(self):

349+261350

"""String representation of the transfer function."""

262351263352

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))

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

601706

def __len__(self):