1
1
# freqplot.py - frequency domain plots for control systems
2
2
#
3
- #Author : Richard M. Murray
3
+ #Initial author : Richard M. Murray
4
4
# Date: 24 May 09
5
5
#
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)
9
12
# [?] 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)
15
13
# [ ] 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
19
14
# [ ] Allow use of subplot labels instead of output/input subtitles
20
- # [i] Add line labels to gangof4 [done by via bode_plot()]
21
15
# [i] Allow frequency range to be overridden in bode_plot
22
16
# [i] Unit tests for discrete time systems with different sample times
23
- # [c] Check examples/bode-and-nyquist-plots.ipynb for differences
24
17
# [ ] Add unit tests for ct.config.defaults['freqplot_number_of_samples']
25
18
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
-
65
19
import numpy as np
66
20
import matplotlib as mpl
67
21
import matplotlib .pyplot as plt
@@ -128,15 +82,12 @@ def plot(self, *args, plot_type=None, **kwargs):
128
82
if plot_type is not None and response .plot_type != plot_type :
129
83
raise TypeError (
130
84
"inconsistent plot_types in data; set plot_type "
131
- "to 'bode' or 'svplot'" )
85
+ "to 'bode', 'nichols', or 'svplot'" )
132
86
plot_type = response .plot_type
133
87
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 )
140
91
141
92
#
142
93
# Bode plot
@@ -1936,23 +1887,7 @@ def _parse_linestyle(style_name, allow_false=False):
1936
1887
ax .grid (color = "lightgray" )
1937
1888
1938
1889
# 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 )
1956
1891
1957
1892
# Add legend if there is more than one system plotted
1958
1893
if len (labels )> 1 :
@@ -2279,6 +2214,9 @@ def singular_values_plot(
2279
2214
(legacy) If given, `singular_values_plot` returns the legacy return
2280
2215
values of magnitude, phase, and frequency. If False, just return
2281
2216
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.
2282
2220
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
2283
2221
Additional keywords passed to `matplotlib` to specify line properties.
2284
2222
@@ -2400,8 +2338,8 @@ def singular_values_plot(
2400
2338
if dB :
2401
2339
with plt .rc_context (freqplot_rcParams ):
2402
2340
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 )
2405
2343
else :
2406
2344
with plt .rc_context (freqplot_rcParams ):
2407
2345
out [idx_sys ]= ax_sigma .loglog (
@@ -2422,26 +2360,10 @@ def singular_values_plot(
2422
2360
ax_sigma .set_xlabel ("Frequency [Hz]" if Hz else "Frequency [rad/sec]" )
2423
2361
2424
2362
# 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 )
2442
2364
2443
2365
# 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 :
2445
2367
with plt .rc_context (freqplot_rcParams ):
2446
2368
ax_sigma .legend (lines ,labels ,loc = legend_loc )
2447
2369
@@ -2649,6 +2571,28 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
2649
2571
return omega
2650
2572
2651
2573
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
+
2652
2596
#
2653
2597
# Utility functions to create nice looking labels (KLD 5/23/11)
2654
2598
#