Skip to content

Commit 51f6bfc

Browse files
committed
allow FrequencyResponseData signal naming + pandas conversion
1 parent e1f8d3a commit 51f6bfc

4 files changed

Lines changed: 94 additions & 13 deletions

File tree

control/frdata.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@
5050
real, imag, absolute, eye, linalg, where, sort
5151
from scipy.interpolate import splprep, splev
5252
from .lti import LTI, _process_frequency_response
53+
from .exception import pandas_check
54+
from .namedio import _NamedIOSystem
5355
from . import config
5456

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

5759

58-
class FrequencyResponseData(LTI):
60+
class FrequencyResponseData(LTI, _NamedIOSystem):
5961
"""FrequencyResponseData(d, w[, smooth])
6062
6163
A class for models defined by frequency response data (FRD).
@@ -152,10 +154,6 @@ def __init__(self, *args, **kwargs):
152154
# TODO: discrete-time FRD systems?
153155
smooth = kwargs.pop('smooth', False)
154156

155-
# Make sure there were no extraneous keywords
156-
if kwargs:
157-
raise TypeError("unrecognized keywords: ", str(kwargs))
158-
159157
if len(args) == 2:
160158
if not isinstance(args[0], FRD) and isinstance(args[0], LTI):
161159
# not an FRD, but still a system, second argument should be
@@ -196,6 +194,23 @@ def __init__(self, *args, **kwargs):
196194
raise ValueError(
197195
"Needs 1 or 2 arguments; received %i." % len(args))
198196

197+
# Set the size of the system
198+
self.noutputs = self.fresp.shape[0]
199+
self.ninputs = self.fresp.shape[1]
200+
201+
# Process signal names
202+
_NamedIOSystem.__init__(
203+
self, name=kwargs.pop('name', None),
204+
inputs=kwargs.pop('inputs', self.ninputs),
205+
outputs=kwargs.pop('outputs', self.noutputs))
206+
207+
# Keep track of return type
208+
self.return_magphase=kwargs.pop('return_magphase', False)
209+
210+
# Make sure there were no extraneous keywords
211+
if kwargs:
212+
raise TypeError("unrecognized keywords: ", str(kwargs))
213+
199214
# create interpolation functions
200215
if smooth:
201216
self.ifunc = empty((self.fresp.shape[0], self.fresp.shape[1]),
@@ -260,11 +275,13 @@ def __add__(self, other):
260275

261276
# Check that the input-output sizes are consistent.
262277
if self.ninputs != other.ninputs:
263-
raise ValueError("The first summand has %i input(s), but the \
264-
second has %i." % (self.ninputs, other.ninputs))
278+
raise ValueError(
279+
"The first summand has %i input(s), but the " \
280+
"second has %i." % (self.ninputs, other.ninputs))
265281
if self.noutputs != other.noutputs:
266-
raise ValueError("The first summand has %i output(s), but the \
267-
second has %i." % (self.noutputs, other.noutputs))
282+
raise ValueError(
283+
"The first summand has %i output(s), but the " \
284+
"second has %i." % (self.noutputs, other.noutputs))
268285

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

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

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

571+
# Convert to pandas
572+
def to_pandas(self):
573+
if not pandas_check():
574+
ImportError('pandas not installed')
575+
import pandas
576+
577+
# Create a dict for setting up the data frame
578+
data = {'omega': self.omega}
579+
data.update(
580+
{'H_{%s, %s}' % (out, inp): self.fresp[i, j] \
581+
for i, out in enumerate(self.output_labels) \
582+
for j, inp in enumerate(self.input_labels)})
583+
584+
return pandas.DataFrame(data)
585+
586+
554587
#
555588
# Allow FRD as an alias for the FrequencyResponseData class
556589
#

control/freqplot.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,9 @@ def bode_plot(syslist, omega=None,
204204
initial_phase = config._get_param(
205205
'freqplot', 'initial_phase', kwargs, None, pop=True)
206206
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)
207+
207208
# If argument was a singleton, turn it into a tuple
208-
if not hasattr(syslist, '__iter__'):
209+
if not isinstance(syslist, (list, tuple)):
209210
syslist = (syslist,)
210211

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

681-
# If argument was a singleton, turn it into a list
682-
if not hasattr(syslist, '__iter__'):
682+
# If argument was a singleton, turn it into a tuple
683+
if not isinstance(syslist, (list, tuple)):
683684
syslist = (syslist,)
684685

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

11111112
# If argument was a singleton, turn it into a tuple
1112-
if not hasattr(syslist, '__iter__'):
1113+
if not isinstance(syslist, (list, tuple)):
11131114
syslist = (syslist,)
11141115

11151116
omega, omega_range_given = _determine_omega_vector(

control/tests/frd_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from control.frdata import FRD, _convert_to_FRD, FrequencyResponseData
1616
from control import bdalg, evalfr, freqplot
1717
from control.tests.conftest import slycotonly
18+
from control.exception import pandas_check
1819

1920

2021
class TestFRD:
@@ -478,3 +479,43 @@ def test_unrecognized_keyword(self):
478479
omega = np.logspace(-1, 2, 10)
479480
with pytest.raises(TypeError, match="unrecognized keyword"):
480481
frd = FRD(h, omega, unknown=None)
482+
483+
484+
def test_named_signals():
485+
ct.namedio._NamedIOSystem._idCounter = 0
486+
h1 = TransferFunction([1], [1, 2, 2])
487+
h2 = TransferFunction([1], [0.1, 1])
488+
omega = np.logspace(-1, 2, 10)
489+
f1 = FRD(h1, omega)
490+
f2 = FRD(h2, omega)
491+
492+
# Make sure that systems were properly named
493+
assert f1.name == 'sys[0]'
494+
assert f2.name == 'sys[1]'
495+
assert f1.ninputs == 1
496+
assert f1.input_labels == ['u[0]']
497+
assert f1.noutputs == 1
498+
assert f1.output_labels == ['y[0]']
499+
500+
# Change names
501+
f1 = FRD(h1, omega, name='mysys', inputs='u0', outputs='y0')
502+
assert f1.name == 'mysys'
503+
assert f1.ninputs == 1
504+
assert f1.input_labels == ['u0']
505+
assert f1.noutputs == 1
506+
assert f1.output_labels == ['y0']
507+
508+
509+
@pytest.mark.skipif(not pandas_check(), reason="pandas not installed")
510+
def test_to_pandas():
511+
# Create a SISO frequency response
512+
h1 = TransferFunction([1], [1, 2, 2])
513+
omega = np.logspace(-1, 2, 10)
514+
resp = FRD(h1, omega)
515+
516+
# Convert to pandas
517+
df = resp.to_pandas()
518+
519+
# Check to make sure the data make senses
520+
np.testing.assert_equal(df['omega'], resp.omega)
521+
np.testing.assert_equal(df['H_{y[0], u[0]}'], resp.fresp[0, 0])

control/tests/type_conversion_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,9 @@ def test_binary_op_type_conversions(opname, ltype, rtype, sys_dict):
185185

186186
# Print out what we are testing in case something goes wrong
187187
assert isinstance(result, type_dict[expected])
188+
189+
# Make sure that input, output, and state names make sense
190+
assert len(result.input_labels) == result.ninputs
191+
assert len(result.output_labels) == result.outputs
192+
if result.nstates is not None:
193+
assert len(result.state_labels) == result.states

0 commit comments

Comments
 (0)