10000 implement ControlPlot class for plotting return type · toaster-code/python-control@ca61be3 · GitHub
[go: up one dir, main page]

Skip to content

Commit ca61be3

Browse files
committed
implement ControlPlot class for plotting return type
1 parent 5f6833f commit ca61be3

16 files changed

+346
-145
lines changed

control/ctrlplot.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
# Collection of functions that are used by various plotting functions.
55

6+
import warnings
67
from os.path import commonprefix
78

89
import matplotlib as mpl
@@ -11,7 +12,7 @@
1112

1213
from . import config
1314

14-
__all__ = ['suptitle', 'get_plot_axes']
15+
__all__ = ['ControlPlot', 'suptitle', 'get_plot_axes']
1516

1617
#
1718
# Style parameters
@@ -28,6 +29,66 @@
2829
})
2930

3031

32+
#
33+
# Control figure
34+
#
35+
36+
class ControlPlot(object):
37+
"""A class for returning control figures.
38+
39+
This class is used as the return type for control plotting functions.
40+
It contains the information required to access portions of the plot
41+
that the user might want to adjust, as well as providing methods to
42+
modify some of the properties of the plot.
43+
44+
A control figure consists of a :class:`matplotlib.figure.Figure` with
45+
an array of :class:`matplotlib.axes.Axes`. Each axes in the figure has
46+
a number of lines that repreesnt the data for the plot. There may also
47+
be a legend present in one or more of the axes.
48+
49+
Attributes
50+
----------
51+
lines : array of list of :class:`matplotlib:Line2D`
52+
Array of Line2D objects for each line in the plot. Generally, The
53+
shape of the array matches the subplots shape and the value of the
54+
array is a list of Line2D objects in that subplot. Some plotting
55+
functions will reeturn variants of this structure, as described in
56+
the individual documentation for the functions.
57+
axes : 2D array of :class:`matplotlib:Axes`
58+
Array of Axes objects for each subplot in the plot.
59+
figure : :class:`matplotlib:Figure`
60+
Figure on which the Axes are drawn.
61+
legend : :class:`matplotlib:Legend` or array of :class:`matplotlib:Legend`
62+
Legend object(s) for the plat. If more than :class:`matplotlib:Legend`
63+
is included, this will be an array with each entry being either
64+
None (for no legend) or a legend object.
65+
66+
"""
67+
def __init__(self, lines, axes=None, figure=None):
68+
self.lines = lines
69+
if axes is None:
70+
axes = get_plot_axes(lines)
71+
self.axes = np.atleast_2d(axes)
72+
if figure is None:
73+
figure = self.axes[0, 0].figure
74+
self.figure = figure
75+
76+
# Implement methods and properties to allow legacy interface (np.array)
77+
__iter__ = lambda self: self.lines
78+
__len__ = lambda self: len(self.lines)
79+
def __getitem__(self, item):
80+
warnings.warn(
81+
"return of Line2D objects from plot function is deprecated in "
82+
"favor of ControlPlot; use out.lines to access Line2D objects",
83+
category=FutureWarning)
84+
return self.lines[item]
85+
def __setitem__(self, item, val):
86+
self.lines[item] = val
87+
shape = property(lambda self: self.lines.shape, None)
88+
def reshape(self, *args):
89+
return self.lines.reshape(*args)
90+
91+
3192
#
3293
# User functions
3394
#
@@ -105,7 +166,10 @@ def get_plot_axes(line_array):
105166
106167
"""
107168
_get_axes = np.vectorize(lambda lines: lines[0].axes)
108-
return _get_axes(line_array)
169+
if isinstance(line_array, ControlPlot):
170+
return _get_axes(line_array.lines)
171+
else:
172+
return _get_axes(line_array)
109173

110174
#
111175
# Utility functions

control/descfcn.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
"""
1414

1515
import math
16-
import numpy as np
16+
from warnings import warn
17+
1718
import matplotlib.pyplot as plt
19+
import numpy as np
1820
import scipy
19-
from warnings import warn
2021

21-
from .freqplot import nyquist_response
2222
from . import config
23+
from .ctrlplot import ControlPlot
24+
from .freqplot import nyquist_response
2325

2426
__all__ = ['describing_function', 'describing_function_plot',
2527
'describing_function_response', 'DescribingFunctionResponse',
@@ -399,7 +401,7 @@ def describing_function_plot(
399401
400402
Parameters
401403
----------
402-
data : :class:`~control.DescribingFunctionData`
404+
data : :class:`~control.DescribingFunctionResponse`
403405
A describing function response data object created by
404406
:func:`~control.describing_function_response`.
405407
H : LTI system
@@ -424,12 +426,23 @@ def describing_function_plot(
424426
425427
Returns
426428
-------
427-
lines : 1D array of Line2D
428-
Arrray of Line2D objects for each line in the plot. The first
429-
element of the array is a list of lines (typically only one) for
430-
the Nyquist plot of the linear I/O styem. The second element of
431-
the array is a list of lines (typically only one) for the
432-
describing function curve.
429+
cplt : :class:`ControlPlot` object
430+
Object containing the data that were plotted:
431+
432+
* cplt.lines: Array of :class:`matplotlib.lines.Line2D` objects
433+
for each line in the plot. The first element of the array is a
434+
list of lines (typically only one) for the Nyquist plot of the
435+
linear I/O system. The second element of the array is a list
436+
of lines (typically only one) for the describing function
437+
curve.
438+
439+
* cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
440+
441+
* cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
442+
443+
* cplt.legend: legend object(s) contained in the plot
444+
445+
See :class:`ControlPlot` for more detailed information.
433446
434447
Examples
435448
--------
@@ -443,6 +456,7 @@ def describing_function_plot(
443456
warn_nyquist = config._process_legacy_keyword(
444457
kwargs, 'warn', 'warn_nyquist', kwargs.pop('warn_nyquist', None))
445458

459+
# TODO: update to be consistent with ctrlplot use of `label`
446460
if label not in (False, None) and not isinstance(label, str):
447461
raise ValueError("label must be formatting string, False, or None")
448462

@@ -454,27 +468,30 @@ def describing_function_plot(
454468
*sysdata, refine=kwargs.pop('refine', True),
455469
warn_nyquist=warn_nyquist)
456470
elif len(sysdata) == 1:
457-
dfresp = sysdata[0]
471+
if not isinstance(sysdata[0], DescribingFunctionResponse):
472+
raise TypeError("data must be DescribingFunctionResponse")
473+
else:
474+
dfresp = sysdata[0]
458475
else:
459476
raise TypeError("1, 3, or 4 position arguments required")
460477

461478
# Create a list of lines for the output
462-
out = np.empty(2, dtype=object)
479+
lines = np.empty(2, dtype=object)
463480

464481
# Plot the Nyquist response
465-
out[0] = dfresp.response.plot(**kwargs)[0]
482+
cfig = dfresp.response.plot(**kwargs)
483+
lines[0] = cfig.lines[0] # Return Nyquist lines for first system
466484

467485
# Add the describing function curve to the plot
468-
lines = plt.plot(dfresp.N_vals.real, dfresp.N_vals.imag)
469-
out[1] = lines
486+
lines[1] = plt.plot(dfresp.N_vals.real, dfresp.N_vals.imag)
470487

471488
# Label the intersection points
472489
if label:
473490
for pos, (a, omega) in zip(dfresp.positions, dfresp.intersections):
474491
# Add labels to the intersection points
475492
plt.text(pos.real, pos.imag, label % (a, omega))
476493

477-
return out
494+
return ControlPlot(lines, cfig.axes, cfig.figure)
478495

479496

480497
# Utility function to figure out whether two line segments intersection

control/freqplot.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from . import config
2121
from .bdalg import feedback
22-
from .ctrlplot import _add_arrows_to_line2D, _ctrlplot_rcParams, \
22+
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _ctrlplot_rcParams, \
2323
_find_axes_center, _get_line_labels, _make_legend_labels, \
2424
_process_ax_keyword, _process_line_labels, _update_suptitle, suptitle
2525
from .ctrlutil import unwrap
@@ -33,7 +33,7 @@
3333
__all__ = ['bode_plot', 'NyquistResponseData', 'nyquist_response',
3434
'nyquist_plot', 'singular_values_response',
3535
'singular_values_plot', 'gangof4_plot', 'gangof4_response',
36-
'bode', 'nyquist', 'gangof4']
36+
'bode', 'nyquist', 'gangof4', 'FrequencyResponseList']
3737

3838
# Default values for module parameter variables< F438 /div>
3939
_freqplot_defaults = {
@@ -124,10 +124,21 @@ def bode_plot(
124124
125125
Returns
126126
-------
127-
lines : array of Line2D
128-
Array of Line2D objects for each line in the plot. The shape of
129-
the array matches the subplots shape and the value of the array is a
130-
list of Line2D objects in that subplot.
127+
cplt : :class:`ControlPlot` object
128+
Object containing the data that were plotted:
129+
130+
* cplt.lines: Array of :class:`matplotlib.lines.Line2D` objects
131+
for each line in the plot. The shape of the array matches the
132+
subplots shape and the value of the array is a list of Line2D
133+
objects in that subplot.
134+
135+
* cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
136+
137+
* cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
138+
139+
* cplt.legend: legend object(s) contained in the plot
140+
141+
See :class:`ControlPlot` for more detailed information.
131142
132143
Other Parameters
133144
----------------
@@ -1008,7 +1019,7 @@ def gen_zero_centered_series(val_min, val_max, period):
10081019
else:
10091020
return mag_data, phase_data, omega_data
10101021

1011-
return out
1022+
return ControlPlot(out, ax_array, fig)
10121023

10131024

10141025
#
@@ -1483,16 +1494,27 @@ def nyquist_plot(
14831494
14841495
Returns
14851496
-------
1486-
lines : array of Line2D
1487-
2D array of Line2D objects for each line in the plot. The shape of
1488-
the array is given by (nsys, 4) where nsys is the number of systems
1489-
or Nyquist responses passed to the function. The second index
1490-
specifies the segment type:
1497+
cplt : :class:`ControlPlot` object
1498+
Object containing the data that were plotted:
1499+
1500+
* cplt.lines: 2D array of :class:`matplotlib.lines.Line2D`
1501+
objects for each line in the plot. The shape of the array is
1502+
given by (nsys, 4) where nsys is the number of systems or
1503+
Nyquist responses passed to the function. The second index
1504+
specifies the segment type:
14911505
1492-
* lines[idx, 0]: unscaled portion of the primary curve
1493-
* lines[idx, 1]: scaled portion of the primary curve
1494-
* lines[idx, 2]: unscaled portion of the mirror curve
1495-
* lines[idx, 3]: scaled portion of the mirror curve
1506+
- lines[idx, 0]: unscaled portion of the primary curve
1507+
- lines[idx, 1]: scaled portion of the primary curve
1508+
- lines[idx, 2]: unscaled portion of the mirror curve
1509+
- lines[idx, 3]: scaled portion of the mirror curve
1510+
1511+
* cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
1512+
1513+
* cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
1514+
1515+
* cplt.legend: legend object(s) contained in the plot
1516+
1517+
See :class:`ControlPlot` for more detailed information.
14961518
14971519
Other Parameters
14981520
----------------
@@ -1923,7 +1945,7 @@ def _parse_linestyle(style_name, allow_false=False):
19231945
# Return counts and (optionally) the contour we used
19241946
return (counts, contours) if return_contour else counts
19251947

1926-
return out
1948+
return ControlPlot(out, ax, fig)
19271949

19281950

19291951
#
@@ -2170,19 +2192,20 @@ def singular_values_plot(
21702192
21712193
Returns
21722194
-------
2173-
legend_loc : str, optional
2174-
For plots with multiple lines, a legend will be included in the
2175-
given location. Default is 'center right'. Use False to suppress.
2176-
lines : array of Line2D
2177-
1-D array of Line2D objects. The size of the array matches
2178-
the number of systems and the value of the array is a list of
2179-
Line2D objects for that system.
2180-
mag : ndarray (or list of ndarray if len(data) > 1))
2181-
If plot=False, magnitude of the response (deprecated).
2182-
phase : ndarray (or list of ndarray if len(data) > 1))
2183-
If plot=False, phase in radians of the response (deprecated).
2184-
omega : ndarray (or list of ndarray if len(data) > 1))
2185-
If plot=False, frequency in rad/sec (deprecated).
2195+
cplt : :class:`ControlPlot` object
2196+
Object containing the data that were plotted:
2197+
2198+
* cplt.lines: 1-D array of :class:`matplotlib.lines.Line2D` objects.
2199+
The size of the array matches the number of systems and the
2200+
value of the array is a list of Line2D objects for that system.
2201+
2202+
* cplt.axes: 2D array of :class:`matplotlib.axes.Axes` for the plot.
2203+
2204+
* cplt.figure: :class:`matplotlib.figure.Figure` containing the plot.
2205+
2206+
* cplt.legend: legend object(s) contained in the plot
2207+
2208+
See :class:`ControlPlot` for more detailed information.
21862209
21872210
Other Parameters
21882211
----------------
@@ -2193,6 +2216,9 @@ def singular_values_plot(
21932216
If present, replace automatically generated label(s) with the given
21942217
label(s). If sysdata is a list, strings should be specified for each
21952218
system.
2219+
legend_loc : str, optional
2220+
For plots with multiple lines, a legend will be included in the
2221+
given location. Default is 'center right'. Use False to supress.
21962222
omega_limits : array_like of two values
21972223
Set limits for plotted frequency range. If Hz=True the limits are
21982224
in Hz otherwise in rad/s. Specifying ``omega`` as a list of two
@@ -2213,6 +2239,16 @@ def singular_values_plot(
22132239
--------
22142240
singular_values_response
22152241
2242+
Notes
2243+
-----
2244+
1. If plot==False, the following legacy values are returned:
2245+
* mag : ndarray (or list of ndarray if len(data) > 1))
2246+
Magnitude of the response (deprecated).
2247+
* phase : ndarray (or list of ndarray if len(data) > 1))
2248+
Phase in radians of the response (deprecated).
2249+
* omega : ndarray (or list of ndarray if len(data) > 1))
2250+
Frequency in rad/sec (deprecated).
2251+
22162252
"""
22172253
# Keyword processing
22182254
dB = config._get_param(
@@ -2363,7 +2399,7 @@ def singular_values_plot(
23632399
else:
23642400
return sigmas, omegas
23652401

2366-
return out
2402+
return ControlPlot(out, ax_sigma, fig)
23672403

23682404
#
23692405
# Utility functions

0 commit comments

Comments
 (0)
0