Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
allow FrequencyResponseData signal naming + pandas conversion
  • Loading branch information
murrayrm committed Apr 2, 2022
commit 51f6bfc21ffb3faa9b7ce90d7a2c5afd68067c85
51 changes: 42 additions & 9 deletions control/frdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@
real, imag, absolute, eye, linalg, where, sort
from scipy.interpolate import splprep, splev
from .lti import LTI, _process_frequency_response
from .exception import pandas_check
from .namedio import _NamedIOSystem
from . import config

__all__ = ['FrequencyResponseData', 'FRD', 'frd']


class FrequencyResponseData(LTI):
class FrequencyResponseData(LTI, _NamedIOSystem):
"""FrequencyResponseData(d, w[, smooth])

A class for models defined by frequency response data (FRD).
Expand Down Expand Up @@ -152,10 +154,6 @@ def __init__(self, *args, **kwargs):
# TODO: discrete-time FRD systems?
smooth = kwargs.pop('smooth', False)

# Make sure there were no extraneous keywords
if kwargs:
raise TypeError("unrecognized keywords: ", str(kwargs))

if len(args) == 2:
if not isinstance(args[0], FRD) and isinstance(args[0], LTI):
# not an FRD, but still a system, second argument should be
Expand Down Expand Up @@ -196,6 +194,23 @@ def __init__(self, *args, **kwargs):
raise ValueError(
"Needs 1 or 2 arguments; received %i." % len(args))

# Set the size of the system
self.noutputs = self.fresp.shape[0]
self.ninputs = self.fresp.shape[1]

# Process signal names
_NamedIOSystem.__init__(
self, name=kwargs.pop('name', None),
inputs=kwargs.pop('inputs', self.ninputs),
outputs=kwargs.pop('outputs', self.noutputs))

# Keep track of return type
self.return_magphase=kwargs.pop('return_magphase', False)

# Make sure there were no extraneous keywords
if kwargs:
raise TypeError("unrecognized keywords: ", str(kwargs))

# create interpolation functions
if smooth:
self.ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
Expand Down Expand Up @@ -260,11 +275,13 @@ def __add__(self, other):

# Check that the input-output sizes are consistent.
if self.ninputs != other.ninputs:
raise ValueError("The first summand has %i input(s), but the \
second has %i." % (self.ninputs, other.ninputs))
raise ValueError(
"The first summand has %i input(s), but the " \
"second has %i." % (self.ninputs, other.ninputs))
if self.noutputs != other.noutputs:
raise ValueError("The first summand has %i output(s), but the \
second has %i." % (self.noutputs, other.noutputs))
raise ValueError(
"The first summand has %i output(s), but the " \
"second has %i." % (self.noutputs, other.noutputs))

return FRD(self.fresp + other.fresp, other.omega)

Expand Down Expand Up @@ -551,6 +568,22 @@ def feedback(self, other=1, sign=-1):

return FRD(fresp, other.omega, smooth=(self.ifunc is not None))

# Convert to pandas
def to_pandas(self):
if not pandas_check():
ImportError('pandas not installed')
import pandas

# Create a dict for setting up the data frame
data = {'omega': self.omega}
data.update(
{'H_{%s, %s}' % (out, inp): self.fresp[i, j] \
for i, out in enumerate(self.output_labels) \
for j, inp in enumerate(self.input_labels)})

return pandas.DataFrame(data)


#
# Allow FRD as an alias for the FrequencyResponseData class
#
Expand Down
9 changes: 5 additions & 4 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ def bode_plot(syslist, omega=None,
initial_phase = config._get_param(
'freqplot', 'initial_phase', kwargs, None, pop=True)
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)

# If argument was a singleton, turn it into a tuple
if not hasattr(syslist, '__iter__'):
if not isinstance(syslist, (list, tuple)):
syslist = (syslist,)

omega, omega_range_given = _determine_omega_vector(
Expand Down Expand Up @@ -678,8 +679,8 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
indent_direction = config._get_param(
'nyquist', 'indent_direction', kwargs, _nyquist_defaults, pop=True)

# If argument was a singleton, turn it into a list
if not hasattr(syslist, '__iter__'):
# If argument was a singleton, turn it into a tuple
if not isinstance(syslist, (list, tuple)):
syslist = (syslist,)

omega, omega_range_given = _determine_omega_vector(
Expand Down Expand Up @@ -1109,7 +1110,7 @@ def singular_values_plot(syslist, omega=None,
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)

# If argument was a singleton, turn it into a tuple
if not hasattr(syslist, '__iter__'):
if not isinstance(syslist, (list, tuple)):
syslist = (syslist,)

omega, omega_range_given = _determine_omega_vector(
Expand Down
41 changes: 41 additions & 0 deletions control/tests/frd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from control.frdata import FRD, _convert_to_FRD, FrequencyResponseData
from control import bdalg, evalfr, freqplot
from control.tests.conftest import slycotonly
from control.exception import pandas_check


class TestFRD:
Expand Down Expand Up @@ -478,3 +479,43 @@ def test_unrecognized_keyword(self):
omega = np.logspace(-1, 2, 10)
with pytest.raises(TypeError, match="unrecognized keyword"):
frd = FRD(h, omega, unknown=None)


def test_named_signals():
ct.namedio._NamedIOSystem._idCounter = 0
h1 = TransferFunction([1], [1, 2, 2])
h2 = TransferFunction([1], [0.1, 1])
omega = np.logspace(-1, 2, 10)
f1 = FRD(h1, omega)
f2 = FRD(h2, omega)

# Make sure that systems were properly named
assert f1.name == 'sys[0]'
assert f2.name == 'sys[1]'
assert f1.ninputs == 1
assert f1.input_labels == ['u[0]']
assert f1.noutputs == 1
assert f1.output_labels == ['y[0]']

# Change names
f1 = FRD(h1, omega, name='mysys', inputs='u0', outputs='y0')
assert f1.name == 'mysys'
assert f1.ninputs == 1
assert f1.input_labels == ['u0']
assert f1.noutputs == 1
assert f1.output_labels == ['y0']


@pytest.mark.skipif(not pandas_check(), reason="pandas not installed")
def test_to_pandas():
# Create a SISO frequency response
h1 = TransferFunction([1], [1, 2, 2])
omega = np.logspace(-1, 2, 10)
resp = FRD(h1, omega)

# Convert to pandas
df = resp.to_pandas()

# Check to make sure the data make senses
np.testing.assert_equal(df['omega'], resp.omega)
np.testing.assert_equal(df['H_{y[0], u[0]}'], resp.fresp[0, 0])
6 changes: 6 additions & 0 deletions control/tests/type_conversion_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,9 @@ def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict):

# Print out what we are testing in case something goes wrong
assert isinstance(result, type_dict[expected])

# Make sure that input, output, and state names make sense
assert len(result.input_labels) == result.ninputs
assert len(result.output_labels) == result.outputs
if result.nstates is not None:
assert len(result.state_labels) == result.states