Skip to content

Commit 2d6a779

Browse files
committed
implement FrequencyResponseList class with plot() method
1 parent 4dbb0cb commit 2d6a779

3 files changed

Lines changed: 51 additions & 10 deletions

File tree

control/frdata.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,9 @@ def __init__(self, *args, **kwargs):
211211
self.sysname = kwargs.pop('sysname', None)
212212

213213
# Keep track of default properties for plotting
214-
self.plot_phase=kwargs.pop('plot_phase', None)
215-
self.title=kwargs.pop('title', None)
214+
self.plot_phase = kwargs.pop('plot_phase', None)
215+
self.title = kwargs.pop('title', None)
216+
self.plot_type = kwargs.pop('plot_type', 'bode')
216217

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

control/freqplot.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,41 @@
111111
'freqplot.share_frequency': 'col',
112112
}
113113

114+
#
115+
# Frequency response data list class
116+
#
117+
# This class is a subclass of list that adds a plot() method, enabling
118+
# direct plotting from routines returning a list of FrequencyResponseData
119+
# objects.
120+
#
121+
122+
class FrequencyResponseList(list):
123+
def plot(self, *args, plot_type=None, **kwargs):
124+
if plot_type == None:
125+
for response in self:
126+
if plot_type is not None and response.plot_type != plot_type:
127+
raise TypeError(
128+
"inconsistent plot_types in data; set plot_type "
129+
"to 'bode', 'svplot', or 'nyquist'")
130+
plot_type = response.plot_type
131+
132+
if plot_type == 'bode':
133+
bode_plot(self, *args, **kwargs)
134+
elif plot_type == 'svplot':
135+
singular_values_plot(self, *args, **kwargs)
136+
elif plot_type == 'nyquist':
137+
# nyquist_plot(self, *args, **kwargs)
138+
raise NotImplementedError("Nyquist plots not yet supported")
139+
else:
140+
raise ValueError(f"unknown plot type '{plot_type}'")
141+
114142
#
115143
# Bode plot
116144
#
145+
# This is the default method for plotting frequency responses. There are
146+
# lots of options available for tuning the format of the plot, (hopefully)
147+
# covering most of the common use cases.
148+
#
117149

118150
def bode_plot(
119151
data, omega=None, *fmt, ax=None, omega_limits=None, omega_num=None,
@@ -653,7 +685,6 @@ def _make_line_label(response, output_index, input_index):
653685
label += ", " if label != "" else ""
654686
label += f"{response.sysname}"
655687

656-
print(label)
657688
return label
658689

659690
for index, response in enumerate(data):
@@ -1927,14 +1958,14 @@ def singular_values_response(
19271958
svd_responses.append(
19281959
FrequencyResponseData(
19291960
sigma_fresp, response.omega, _return_singvals=True,
1930-
outputs=[f'$\\sigma_{k}$' for k in range(sigma.shape[0])],
1961+
outputs=[f'$\\sigma_{{{k+1}}}$' for k in range(sigma.shape[0])],
19311962
inputs='inputs', dt=response.dt, plot_phase=False,
1932-
sysname=response.sysname,
1963+
sysname=response.sysname, plot_type='svplot',
19331964
title=f"Singular values for {response.sysname}"))
19341965

19351966
# Return the responses in the same form that we received the systems
19361967
if isinstance(sys, (list, tuple)):
1937-
return svd_responses
1968+
return FrequencyResponseList(svd_responses)
19381969
else:
19391970
return svd_responses[0]
19401971

@@ -2017,6 +2048,11 @@ def singular_values_plot(
20172048
else:
20182049
plot = True
20192050

2051+
# Warn the user if we got past something that is not real-valued
2052+
if any([not np.allclose(np.imag(response.fresp[:, 0, :]), 0)
2053+
for response in responses]):
2054+
warnings.warn("data has non-zero imaginary component")
2055+
20202056
# Extract the data we need for plotting
20212057
sigmas = [np.real(response.fresp[:, 0, :]) for response in responses]
20222058
omegas = [response.omega for response in responses]

control/lti.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def frequency_response(self, omega=None, squeeze=None):
119119
# Return the data as a frequency response data object
120120
response = self(s)
121121
return FrequencyResponseData(
122-
response, omega, return_magphase=True, squeeze=squeeze, dt=self.dt,
123-
sysname=self.name)
122+
response, omega, return_magphase=True, squeeze=squeeze,
123+
dt=self.dt, sysname=self.name, plot_type='bode')
124124

125125
def dcgain(self):
126126
"""Return the zero-frequency gain"""
@@ -470,8 +470,12 @@ def frequency_response(
470470

471471
# Compute the frequency response
472472
responses.append(sys_.frequency_response(omega_sys, squeeze=squeeze))
473-
474-
return responses if isinstance(sys, (list, tuple)) else responses[0]
473+
474+
if isinstance(sys, (list, tuple)):
475+
from .freqplot import FrequencyResponseList
476+
return FrequencyResponseList(responses)
477+
else:
478+
return responses[0]
475479

476480
# Alternative name (legacy)
477481
def freqresp(sys, omega):

0 commit comments

Comments
 (0)