Skip to content

Commit de5ea5a

Browse files
committed
refactoring of nichols into response/plot + updated unit tests, examples
1 parent 3496141 commit de5ea5a

File tree

9 files changed

+153
-161
lines changed

9 files changed

+153
-161
lines changed

LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Copyright (c) 2009-2016 by California Institute of Technology
2+
Copyright (c) 2016-2023 by python-control developers
23
All rights reserved.
34

45
Redistribution and use in source and binary forms, with or without

control/frdata.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,12 +667,15 @@ def plot(self, plot_type=None, *args, **kwargs):
667667
668668
"""
669669
from .freqplot import bode_plot, singular_values_plot
670+
from .nichols import nichols_plot
670671

671672
if plot_type is None:
672673
plot_type = self.plot_type
673674

674675
if plot_type == 'bode':
675676
return bode_plot(self, *args, **kwargs)
677+
elif plot_type == 'nichols':
678+
return nichols_plot(self, *args, **kwargs)
676679
elif plot_type == 'svplot':
677680
return singular_values_plot(self, *args, **kwargs)
678681
else:

control/freqplot.py

Lines changed: 41 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,21 @@
11
# freqplot.py - frequency domain plots for control systems
22
#
3-
# Author: Richard M. Murray
3+
# Initial author: Richard M. Murray
44
# Date: 24 May 09
55
#
6-
# Functionality to add
7-
# [ ] Get rid of this long header (need some common, documented convention)
8-
# [x] Add mechanisms for storing/plotting margins? (currently forces FRD)
6+
# This file contains some standard control system plots: Bode plots,
7+
# Nyquist plots and other frequency response plots. The code for Nichols
8+
# charts is in nichols.py. The code for pole-zero diagrams is in pzmap.py
9+
# and rlocus.py.
10+
#
11+
# Functionality to add/check (Jul 2023, working list)
912
# [?] 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 (?)
12-
# [i] Allow MIMO frequency plots (w/ mag/phase subplots a la MATLAB)
13-
# [i] Update sisotool to use ax=
14-
# [i] Create __main__ in freqplot_test to view results (a la timeplot_test)
1513
# [ ] Get sisotool working in iPython and document how to make it work
16-
# [i] Allow share_magnitude, share_phase, share_frequency keywords for units
17-
# [i] Re-implement including of gain/phase margin in the title (?)
18-
# [i] Change gangof4 to use bode_plot(plot_phase=False) w/ proper labels
1914
# [ ] Allow use of subplot labels instead of output/input subtitles
20-
# [i] Add line labels to gangof4 [done by via bode_plot()]
2115
# [i] Allow frequency range to be overridden in bode_plot
2216
# [i] Unit tests for discrete time systems with different sample times
23-
# [c] Check examples/bode-and-nyquist-plots.ipynb for differences
2417
# [ ] Add unit tests for ct.config.defaults['freqplot_number_of_samples']
2518

26-
#
27-
# This file contains some standard control system plots: Bode plots,
28-
# Nyquist plots and other frequency response plots. The code for Nichols
29-
# charts is in nichols.py. The code for pole-zero diagrams is in pzmap.py
30-
# and rlocus.py.
31-
#
32-
# Copyright (c) 2010 by California Institute of Technology
33-
# All rights reserved.
34-
#
35-
# Redistribution and use in source and binary forms, with or without
36-
# modification, are permitted provided that the following conditions
37-
# are met:
38-
#
39-
# 1. Redistributions of source code must retain the above copyright
40-
# notice, this list of conditions and the following disclaimer.
41-
#
42-
# 2. Redistributions in binary form must reproduce the above copyright
43-
# notice, this list of conditions and the following disclaimer in the
44-
# documentation and/or other materials provided with the distribution.
45-
#
46-
# 3. Neither the name of the California Institute of Technology nor
47-
# the names of its contributors may be used to endorse or promote
48-
# products derived from this software without specific prior
49-
# written permission.
50-
#
51-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
52-
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
53-
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
54-
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH
55-
# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
56-
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
57-
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
58-
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
59-
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
60-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
61-
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
62-
# SUCH DAMAGE.
63-
#
64-
6519
import numpy as np
6620
import matplotlib as mpl
6721
import matplotlib.pyplot as plt
@@ -128,15 +82,12 @@ def plot(self, *args, plot_type=None, **kwargs):
12882
if plot_type is not None and response.plot_type != plot_type:
12983
raise TypeError(
13084
"inconsistent plot_types in data; set plot_type "
131-
"to 'bode' or 'svplot'")
85+
"to 'bode', 'nichols', or 'svplot'")
13286
plot_type = response.plot_type
13387

134-
if plot_type == 'bode':
135-
return bode_plot(self, *args, **kwargs)
136-
elif plot_type == 'svplot':
137-
return singular_values_plot(self, *args, **kwargs)
138-
else:
139-
raise ValueError(f"unknown plot type '{plot_type}'")
88+
# Use FRD plot method, which can handle lists via plot functions
89+
return FrequencyResponseData.plot(
90+
self, plot_type=plot_type, *args, **kwargs)
14091

14192
#
14293
# Bode plot
@@ -1936,23 +1887,7 @@ def _parse_linestyle(style_name, allow_false=False):
19361887
ax.grid(color="lightgray")
19371888

19381889
# List of systems that are included in this plot
1939-
labels, lines = [], []
1940-
last_color, counter = None, 0 # label unknown systems
1941-
for i, line in enumerate(ax.get_lines()):
1942-
label = line.get_label()
1943-
if label.startswith("Unknown"):
1944-
label = f"Unknown-{counter}"
1945-
if last_color is None:
1946-
last_color = line.get_color()
1947-
elif last_color != line.get_color():
1948-
counter += 1
1949-
last_color = line.get_color()
1950-
elif label[0] == '_':
1951-
continue
1952-
1953-
if label not in labels:
1954-
lines.append(line)
1955-
labels.append(label)
1890+
lines, labels = _get_line_labels(ax)
19561891

19571892
# Add legend if there is more than one system plotted
19581893
if len(labels) > 1:
@@ -2279,6 +2214,9 @@ def singular_values_plot(
22792214
(legacy) If given, `singular_values_plot` returns the legacy return
22802215
values of magnitude, phase, and frequency. If False, just return
22812216
the values with no plot.
2217+
legend_loc : str, optional
2218+
For plots with multiple lines, a legend will be included in the
2219+
given location. Default is 'center right'. Use False to supress.
22822220
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
22832221
Additional keywords passed to `matplotlib` to specify line properties.
22842222
@@ -2400,8 +2338,8 @@ def singular_values_plot(
24002338
if dB:
24012339
with plt.rc_context(freqplot_rcParams):
24022340
out[idx_sys] = ax_sigma.semilogx(
2403-
omega_plot, 20 * np.log10(sigma_plot), color=color,
2404-
label=sysname, *fmt, **kwargs)
2341+
omega_plot, 20 * np.log10(sigma_plot), *fmt, color=color,
2342+
label=sysname, **kwargs)
24052343
else:
24062344
with plt.rc_context(freqplot_rcParams):
24072345
out[idx_sys] = ax_sigma.loglog(
@@ -2422,26 +2360,10 @@ def singular_values_plot(
24222360
ax_sigma.set_xlabel("Frequency [Hz]" if Hz else "Frequency [rad/sec]")
24232361

24242362
# List of systems that are included in this plot
2425-
labels, lines = [], []
2426-
last_color, counter = None, 0 # label unknown systems
2427-
for i, line in enumerate(ax_sigma.get_lines()):
2428-
label = line.get_label()
2429-
if label.startswith("Unknown"):
2430-
label = f"Unknown-{counter}"
2431-
if last_color is None:
2432-
last_color = line.get_color()
2433-
elif last_color != line.get_color():
2434-
counter += 1
2435-
last_color = line.get_color()
2436-
elif label[0] == '_':
2437-
continue
2438-
2439-
if label not in labels:
2440-
lines.append(line)
2441-
labels.append(label)
2363+
lines, labels = _get_line_labels(ax_sigma)
24422364

24432365
# Add legend if there is more than one system plotted
2444-
if len(labels) > 1:
2366+
if len(labels) > 1 and legend_loc is not False:
24452367
with plt.rc_context(freqplot_rcParams):
24462368
ax_sigma.legend(lines, labels, loc=legend_loc)
24472369

@@ -2649,6 +2571,28 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
26492571
return omega
26502572

26512573

2574+
# Get labels for all lines in an axes
2575+
def _get_line_labels(ax, use_color=True):
2576+
labels, lines = [], []
2577+
last_color, counter = None, 0 # label unknown systems
2578+
for i, line in enumerate(ax.get_lines()):
2579+
label = line.get_label()
2580+
if use_color and label.startswith("Unknown"):
2581+
label = f"Unknown-{counter}"
2582+
if last_color is None:
2583+
last_color = line.get_color()
2584+
elif last_color != line.get_color():
2585+
counter += 1
2586+
last_color = line.get_color()
2587+
elif label[0] == '_':
2588+
continue
2589+
2590+
if label not in labels:
2591+
lines.append(line)
2592+
labels.append(label)
2593+
2594+
return lines, labels
2595+
26522596
#
26532597
# Utility functions to create nice looking labels (KLD 5/23/11)
26542598
#

0 commit comments

Comments
 (0)