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-
6519import numpy as np
6620import matplotlib as mpl
6721import 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