Skip to content

Commit 39a976f

Browse files
committed
regularize sysdata/list processing + small refactor + updated example
1 parent 4802217 commit 39a976f

File tree

4 files changed

+6644
-5427
lines changed

4 files changed

+6644
-5427
lines changed

control/freqplot.py

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
#
66
# Functionality to add
77
# [ ] Get rid of this long header (need some common, documented convention)
8-
# [ ] Add mechanisms for storing/plotting margins? (currently forces FRD)
9-
# [ ] Allow line colors/styles to be set in plot() command (also time plots)
10-
# [ ] Allow bode or nyquist style plots from plot()
11-
# [ ] Allow nyquist_response() to generate the response curve (?)
8+
# [x] Add mechanisms for storing/plotting margins? (currently forces FRD)
9+
# [?] Allow line colors/styles to be set in plot() command (also time plots)
10+
# [x] Allow bode or nyquist style plots from plot()
11+
# [i] Allow nyquist_response() to generate the response curve (?)
1212
# [i] Allow MIMO frequency plots (w/ mag/phase subplots a la MATLAB)
1313
# [i] Update sisotool to use ax=
1414
# [i] Create __main__ in freqplot_test to view results (a la timeplot_test)
@@ -17,11 +17,11 @@
1717
# [i] Re-implement including of gain/phase margin in the title (?)
1818
# [i] Change gangof4 to use bode_plot(plot_phase=False) w/ proper labels
1919
# [ ] Allow use of subplot labels instead of output/input subtitles
20-
# [ ] Add line labels to gangof4
21-
# [ ] Update FRD to allow nyquist_response contours
22-
# [ ] Allow frequency range to be overridden in bode_plot
23-
# [ ] Unit tests for discrete time systems with different sample times
24-
# [ ] Check examples/bode-and-nyquist-plots.ipynb for differences
20+
# [i] Add line labels to gangof4 [done by via bode_plot()]
21+
# [i] Allow frequency range to be overridden in bode_plot
22+
# [i] Unit tests for discrete time systems with different sample times
23+
# [c] Check examples/bode-and-nyquist-plots.ipynb for differences
24+
# [ ] Add unit tests for ct.config.defaults['freqplot_number_of_samples']
2525

2626
#
2727
# This file contains some standard control system plots: Bode plots,
@@ -704,7 +704,7 @@ def _make_line_label(response, output_index, input_index):
704704
# Get the frequencies and convert to Hz, if needed
705705
omega_plot = omega_sys / (2 * math.pi) if Hz else omega_sys
706706
if response.isdtime(strict=True):
707-
nyq_freq = 0.5 /response.dt if Hz else math.pi / response.dt
707+
nyq_freq = (0.5/response.dt) if Hz else (math.pi/response.dt)
708708

709709
# Save the magnitude and phase to plot
710710
mag_plot = 20 * np.log10(mag[i, j]) if dB else mag[i, j]
@@ -1163,7 +1163,7 @@ def plot(self, *args, **kwargs):
11631163

11641164

11651165
def nyquist_response(
1166-
syslist, omega=None, plot=None, omega_limits=None, omega_num=None,
1166+
sysdata, omega=None, plot=None, omega_limits=None, omega_num=None,
11671167
label_freq=0, color=None, return_contour=False, check_kwargs=True,
11681168
warn_encirclements=True, warn_nyquist=True, **kwargs):
11691169
"""Nyquist response for a system.
@@ -1179,7 +1179,7 @@ def nyquist_response(
11791179
11801180
Parameters
11811181
----------
1182-
syslist : list of LTI
1182+
sysdata : LTI or list of LTI
11831183
List of linear input/output systems (single system is OK). Nyquist
11841184
curves for each system are plotted on the same graph.
11851185
@@ -1283,9 +1283,8 @@ def nyquist_response(
12831283
if check_kwargs and kwargs:
12841284
raise TypeError("unrecognized keywords: ", str(kwargs))
12851285

1286-
# If argument was a singleton, turn it into a tuple
1287-
if not isinstance(syslist, (list, tuple)):
1288-
syslist = (syslist,)
1286+
# Convert the first argument to a list
1287+
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
12891288

12901289
# Determine the range of frequencies to use, based on args/features
12911290
omega, omega_range_given = _determine_omega_vector(
@@ -1499,17 +1498,15 @@ def nyquist_response(
14991498
count, contour, resp, sys.dt, sysname=sysname,
15001499
return_contour=return_contour))
15011500

1502-
# Return response
1503-
if len(responses) == 1: # TODO: update to match input type
1504-
return responses[0]
1505-
else:
1501+
if isinstance(sysdata, (list, tuple)):
15061502
return NyquistResponseList(responses)
1503+
else:
1504+
return responses[0]
15071505

15081506

15091507
def nyquist_plot(
1510-
data, omega=None, plot=None, omega_limits=None, omega_num=None,
1511-
label_freq=0, color=None, return_contour=None, title=None,
1512-
legend_loc='upper right', **kwargs):
1508+
data, omega=None, plot=None, label_freq=0, color=None,
1509+
return_contour=None, title=None, legend_loc='upper right', **kwargs):
15131510
"""Nyquist plot for a system.
15141511
15151512
Generates a Nyquist plot for the system over a (optional) frequency
@@ -1677,8 +1674,6 @@ def nyquist_plot(
16771674
16781675
"""
16791676
# Get values for params (and pop from list to allow keyword use in plot)
1680-
omega_num_given = omega_num is not None
1681-
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)
16821677
arrows = config._get_param(
16831678
'nyquist', 'arrows', kwargs, _nyquist_defaults, pop=True)
16841679
arrow_size = config._get_param(
@@ -1724,18 +1719,21 @@ def _parse_linestyle(style_name, allow_false=False):
17241719
else:
17251720
raise ValueError("unknown or unsupported arrow location")
17261721

1722+
# Set the arrow style
1723+
if arrow_style is None:
1724+
arrow_style = mpl.patches.ArrowStyle(
1725+
'simple', head_width=arrow_size, head_length=arrow_size)
1726+
17271727
# If argument was a singleton, turn it into a tuple
17281728
if not isinstance(data, (list, tuple)):
17291729
data = (data,)
17301730

17311731
# If we are passed a list of systems, compute response first
1732-
# If we were passed a list of systems, convert to data
17331732
if all([isinstance(
17341733
sys, (StateSpace, TransferFunction, FrequencyResponseData))
17351734
for sys in data]):
17361735
nyquist_responses = nyquist_response(
1737-
data, omega=omega, omega_limits=omega_limits, omega_num=omega_num,
1738-
check_kwargs=False, **kwargs)
1736+
data, omega=omega, check_kwargs=False, **kwargs)
17391737
if not isinstance(nyquist_responses, list):
17401738
nyquist_responses = [nyquist_responses]
17411739
else:
@@ -1763,11 +1761,6 @@ def _parse_linestyle(style_name, allow_false=False):
17631761
for i in range(out.shape[0]):
17641762
out[i] = [] # unique list in each element
17651763

1766-
# Set the arrow style
1767-
if arrow_style is None:
1768-
arrow_style = mpl.patches.ArrowStyle(
1769-
'simple', head_width=arrow_size, head_length=arrow_size)
1770-
17711764
for idx, response in enumerate(nyquist_responses):
17721765
resp = response.response
17731766
if response.dt in [0, None]:
@@ -1919,6 +1912,7 @@ def _parse_linestyle(style_name, allow_false=False):
19191912
title = "Nyquist plot for " + ", ".join(labels)
19201913
fig.suptitle(title)
19211914

1915+
# Legacy return pocessing
19221916
if plot is True or return_contour is not None:
19231917
if len(data) == 1:
19241918
counts, contours = counts[0], contours[0]
@@ -2134,7 +2128,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
21342128
# Singular values plot
21352129
#
21362130
def singular_values_response(
2137-
sys, omega=None, omega_limits=None, omega_num=None, Hz=False):
2131+
sysdata, omega=None, omega_limits=None, omega_num=None, Hz=False):
21382132
"""Singular value response for a system.
21392133
21402134
Computes the singular values for a system or list of systems over
@@ -2173,8 +2167,8 @@ def singular_values_response(
21732167
>>> response = ct.singular_values_response(G, omega=omegas)
21742168
21752169
"""
2176-
# If argument was a singleton, turn it into a tuple
2177-
syslist = sys if isinstance(sys, (list, tuple)) else (sys,)
2170+
# Convert the first argument to a list
2171+
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
21782172

21792173
if any([not isinstance(sys, LTI) for sys in syslist]):
21802174
ValueError("singular values can only be computed for LTI systems")
@@ -2201,8 +2195,7 @@ def singular_values_response(
22012195
sysname=response.sysname, plot_type='svplot',
22022196
title=f"Singular values for {response.sysname}"))
22032197

2204-
# Return the responses in the same form that we received the systems
2205-
if isinstance(sys, (list, tuple)):
2198+
if isinstance(sysdata, (list, tuple)):
22062199
return FrequencyResponseList(svd_responses)
22072200
else:
22082201
return svd_responses[0]
@@ -2554,20 +2547,20 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
25542547
if np.any(toreplace):
25552548
features_ = features_[~toreplace]
25562549
elif sys.isdtime(strict=True):
2557-
fn = math.pi * 1. / sys.dt
2550+
fn = math.pi / sys.dt
25582551
# TODO: What distance to the Nyquist frequency is appropriate?
25592552
freq_interesting.append(fn * 0.9)
25602553

25612554
features_ = np.concatenate((sys.poles(), sys.zeros()))
25622555
# Get rid of poles and zeros on the real axis (imag==0)
2563-
# * origin and real < 0
2556+
# * origin and real < 0
25642557
# * at 1.: would result in omega=0. (logaritmic plot!)
25652558
toreplace = np.isclose(features_.imag, 0.0) & (
25662559
(features_.real <= 0.) |
25672560
(np.abs(features_.real - 1.0) < 1.e-10))
25682561
if np.any(toreplace):
25692562
features_ = features_[~toreplace]
2570-
# TODO: improve
2563+
# TODO: improve (mapping pack to continuous time)
25712564
features_ = np.abs(np.log(features_) / (1.j * sys.dt))
25722565
else:
25732566
# TODO

control/lti.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def evalfr(sys, x, squeeze=None):
372372

373373

374374
def frequency_response(
375-
sys, omega=None, omega_limits=None, omega_num=None,
375+
sysdata, omega=None, omega_limits=None, omega_num=None,
376376
Hz=None, squeeze=None):
377377
"""Frequency response of an LTI system at multiple angular frequencies.
378378
@@ -382,7 +382,7 @@ def frequency_response(
382382
383383
Parameters
384384
----------
385-
sys: LTI system or list of LTI systems
385+
sysdata: LTI system or list of LTI systems
386386
Linear system(s) for which frequency response is computed.
387387
omega : float or 1D array_like, optional
388388
A list of frequencies in radians/sec at which the system should be
@@ -452,8 +452,11 @@ def frequency_response(
452452
"""
453453
from .freqplot import _determine_omega_vector
454454

455+
# Process keyword arguments
456+
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)
457+
455458
# Convert the first argument to a list
456-
syslist = sys if isinstance(sys, (list, tuple)) else [sys]
459+
syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata]
457460

458461
# Get the common set of frequencies to use
459462
omega_syslist, omega_range_given = _determine_omega_vector(
@@ -472,7 +475,7 @@ def frequency_response(
472475
# Compute the frequency response
473476
responses.append(sys_.frequency_response(omega_sys, squeeze=squeeze))
474477

475-
if isinstance(sys, (list, tuple)):
478+
if isinstance(sysdata, (list, tuple)):
476479
from .freqplot import FrequencyResponseList
477480
return FrequencyResponseList(responses)
478481
else:

control/tests/freqplot_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,30 @@ def test_gangof4_plots(savefigs=False):
179179
if savefigs:
180180
plt.savefig('freqplot-gangof4.png')
181181

182+
@pytest.mark.parametrize("response_cmd, return_type", [
183+
(ct.frequency_response, ct.FrequencyResponseData),
184+
(ct.nyquist_response, ct.freqplot.NyquistResponseData),
185+
(ct.singular_values_response, ct.FrequencyResponseData),
186+
])
187+
def test_first_arg_listable(response_cmd, return_type):
188+
sys = ct.rss(2, 1, 1)
189+
190+
# If we pass a single system, should get back a single system
191+
result = response_cmd(sys)
192+
assert isinstance(result, return_type)
193+
194+
# If we pass a list of systems, we should get back a list
195+
result = response_cmd([sys, sys, sys])
196+
assert isinstance(result, list)
197+
assert len(result) == 3
198+
assert all([isinstance(item, return_type) for item in result])
199+
200+
# If we pass a singleton list, we should get back a list
201+
result = response_cmd([sys])
202+
assert isinstance(result, list)
203+
assert len(result) == 1
204+
assert isinstance(result[0], return_type)
205+
182206

183207
if __name__ == "__main__":
184208
#

0 commit comments

Comments
 (0)