Skip to content

Commit 829b624

Browse files
committed
update unit tests and documentation for line properties
1 parent d815b3a commit 829b624

File tree

10 files changed

+191
-232
lines changed

10 files changed

+191
-232
lines changed

control/ctrlplot.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,3 +654,32 @@ def _add_arrows_to_line2D(
654654
axes.add_patch(p)
655655
arrows.append(p)
656656
return arrows
657+
658+
659+
def _get_color(colorspec, ax=None, lines=None, color_cycle=None):
660+
# See if the color was explicitly specified by the user
661+
if isinstance(colorspec, dict):
662+
if 'color' in colorspec:
663+
return colorspec.pop('color')
664+
elif colorspec != None:
665+
return colorspec
666+
667+
# Figure out what color cycle to use, if not given by caller
668+
if color_cycle == None:
669+
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
670+
671+
# Find the lines that we should pay attention to
672+
if lines is None and ax is not None:
673+
lines = ax.lines
674+
675+
# If we were passed a set of lines, try to increment color from previous
676+
if lines is not None:
677+
color_offset = 0
678+
if len(ax.lines) > 0:
679+
last_color = ax.lines[-1].get_color()
680+
if last_color in color_cycle:
681+
color_offset = color_cycle.index(last_color) + 1
682+
color_offset = color_offset % len(color_cycle)
683+
return color_cycle[color_offset]
684+
else:
685+
return None

control/descfcn.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@ def describing_function_plot(
429429
Otherwise, a new figure is created.
430430
title : str, optional
431431
Set the title of the plot. Defaults to plot type and system name(s).
432+
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
433+
Additional keywords passed to `matplotlib` to specify line properties
434+
for Nyquist curve.
432435
433436
Returns
434437
-------

control/freqplot.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,8 +1505,6 @@ def nyquist_plot(
15051505
Set of frequencies to be evaluated, in rad/sec. Specifying
15061506
``omega`` as a list of two elements is equivalent to providing
15071507
``omega_limits``.
1508-
color : string, optional
1509-
Used to specify the color of the line and arrowhead.
15101508
unit_circle : bool, optional
15111509
If ``True``, display the unit circle, to read gain crossover frequency.
15121510
mt_circles : array_like, optional
@@ -1515,7 +1513,7 @@ def nyquist_plot(
15151513
Draw circles corresponding to the given magnitudes of complementary
15161514
sensitivity.
15171515
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
1518-
Additional keywords (passed to `matplotlib`)
1516+
Additional keywords passed to `matplotlib` to specify line properties.
15191517
15201518
Returns
15211519
-------

control/phaseplot.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from scipy.integrate import odeint
3737

3838
from . import config
39-
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, \
39+
from .ctrlplot import ControlPlot, _add_arrows_to_line2D, _get_color, \
4040
_process_ax_keyword, _update_plot_title
4141
from .exception import ControlNotImplemented
4242
from .nlsys import NonlinearIOSystem, find_eqpt, input_output_response
@@ -90,7 +90,7 @@ def phase_plane_plot(
9090
Parameters to pass to system. For an I/O system, `params` should be
9191
a dict of parameters and values. For a callable, `params` should be
9292
dict with key 'args' and value given by a tuple (passed to callable).
93-
color : str
93+
color : matplotlib color spec, optional
9494
Plot all elements in the given color (use `plot_<fcn>={'color': c}`
9595
to set the color in one element of the phase plot.
9696
ax : matplotlib.axes.Axes, optional
@@ -103,7 +103,7 @@ def phase_plane_plot(
103103
cplt : :class:`ControlPlot` object
104104
Object containing the data that were plotted:
105105
106-
* cplt.lines: list of list of :class:`matplotlib.artist.Artist`
106+
* cplt.lines: array of list of :class:`matplotlib.artist.Artist`
107107
objects:
108108
109109
- lines[0] = list of Line2D objects (streamlines, separatrices).
@@ -153,6 +153,7 @@ def phase_plane_plot(
153153

154154
# Create copy of kwargs for later checking to find unused arguments
155155
initial_kwargs = dict(kwargs)
156+
passed_kwargs = False
156157

157158
# Utility function to create keyword arguments
158159
def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs):
@@ -163,7 +164,7 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs):
163164
return new_kwargs
164165

165166
# Create list for storing outputs
166-
out = [[], None, None]
167+
out = np.array([[], None, None], dtype=object)
167168

168169
# Plot out the main elements
169170
if plot_streamlines:
@@ -217,7 +218,6 @@ def _create_kwargs(global_kwargs, local_kwargs, **other_kwargs):
217218
if initial_kwargs:
218219
raise TypeError("unrecognized keywords: ", str(initial_kwargs))
219220

220-
# TODO: update to common code pattern
221221
if user_ax is None:
222222
if title is None:
223223
title = f"Phase portrait for {sys.name}"
@@ -263,7 +263,7 @@ def vectorfield(
263263
Parameters to pass to system. For an I/O system, `params` should be
264264
a dict of parameters and values. For a callable, `params` should be
265265
dict with key 'args' and value given by a tuple (passed to callable).
266-
color : str
266+
color : matplotlib color spec, optional
267267
Plot the vector field in the given color.
268268
ax : matplotlib.axes.Axes
269269
Use the given axes for the plot, otherwise use the current axes.
@@ -298,7 +298,7 @@ def vectorfield(
298298
xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
299299

300300
# Figure out the color to use
301-
color = _get_color(kwargs, ax)
301+
color = _get_color(kwargs, ax=ax)
302302

303303
# Make sure all keyword arguments were processed
304304
if check_kwargs and kwargs:
@@ -396,7 +396,7 @@ def streamlines(
396396
xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
397397

398398
# Figure out the color to use
399-
color = _get_color(kwargs, ax)
399+
color = _get_color(kwargs, ax=ax)
400400

401401
# Make sure all keyword arguments were processed
402402
if check_kwargs and kwargs:
@@ -424,12 +424,11 @@ def streamlines(
424424
# Plot the trajectory (if there is one)
425425
if traj.shape[1] > 1:
426426
with plt.rc_context(rcParams):
427-
out.append(
428-
ax.plot(traj[0], traj[1], color=color))
427+
out += ax.plot(traj[0], traj[1], color=color)
429428

430429
# Add arrows to the lines at specified intervals
431430
_add_arrows_to_line2D(
432-
ax, out[-1][0], arrow_pos, arrowstyle=arrow_style, dir=1)
431+
ax, out[-1], arrow_pos, arrowstyle=arrow_style, dir=1)
433432
return out
434433

435434

@@ -592,7 +591,7 @@ def separatrices(
592591
xlim, ylim, maxlim = _set_axis_limits(ax, pointdata)
593592

594593
# Figure out the color to use for stable, unstable subspaces
595-
color = _get_color(kwargs)
594+
color = _get_color(kwargs, ax=ax)
596595
match color:
597596
case None:
598597
stable_color = 'r'
@@ -924,23 +923,6 @@ def _parse_arrow_keywords(kwargs):
924923

925924

926925
# TODO: move to ctrlplot?
927-
def _get_color(kwargs, ax=None):
928-
if 'color' in kwargs:
929-
return kwargs.pop('color')
930-
931-
# If we were passed an axis, try to increment color from previous
932-
color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']
933-
if ax is not None:
934-
color_offset = 0
935-
if len(ax.lines) > 0:
936-
last_color = ax.lines[-1].get_color()
937-
if last_color in color_cycle:
938-
color_offset = color_cycle.index(last_color) + 1
939-
return color_cycle[color_offset % len(color_cycle)]
940-
else:
941-
return None
942-
943-
944926
def _create_trajectory(
945927
sys, revsys, timepts, X0, params, dir, suppress_warnings=False,
946928
gridtype=None, gridspec=None, xlim=None, ylim=None):

control/pzmap.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from numpy import cos, exp, imag, linspace, real, sin, sqrt
1919

2020
from . import config
21+
from .config import _process_legacy_keyword
2122
from .ctrlplot import ControlPlot, _get_line_labels, _process_ax_keyword, \
2223
_process_legend_keywords, _process_line_labels, _update_plot_title
2324
from .freqplot import _freqplot_defaults
@@ -170,10 +171,9 @@ def pole_zero_map(sysdata):
170171
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_axisline_style.html
171172
# https://matplotlib.org/2.0.2/examples/axes_grid/demo_curvelinear_grid.html
172173
def pole_zero_plot(
173-
data, plot=None, grid=None, title=None, marker_color=None,
174-
marker_size=None, marker_width=None,
175-
xlim=None, ylim=None, interactive=None, ax=None, scaling=None,
176-
initial_gain=None, label=None, **kwargs):
174+
data, plot=None, grid=None, title=None, color=None, marker_size=None,
175+
marker_width=None, xlim=None, ylim=None, interactive=None, ax=None,
176+
scaling=None, initial_gain=None, label=None, **kwargs):
177177
"""Plot a pole/zero map for a linear system.
178178
179179
If the system data include root loci, a root locus diagram for the
@@ -231,9 +231,10 @@ def pole_zero_plot(
231231
The matplotlib axes to draw the figure on. If not specified and
232232
the current figure has a single axes, that axes is used.
233233
Otherwise, a new figure is created.
234+
color : matplotlib color spec, optional
235+
Specify the color of the markers and lines.
234236
initial_gain : float, optional
235237
If given, the specified system gain will be marked on the plot.
236-
237238
interactive : bool, optional
238239
Turn off interactive mode for root locus plots.
239240
label : str or array_like of str, optional
@@ -281,6 +282,7 @@ def pole_zero_plot(
281282
label = _process_line_labels(label)
282283
marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6)
283284
marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5)
285+
user_color = _process_legacy_keyword(kwargs, 'marker_color', 'color', color)
284286
rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
285287
user_ax = ax
286288
xlim_user, ylim_user = xlim, ylim
@@ -387,10 +389,10 @@ def pole_zero_plot(
387389
zeros = response.zeros
388390

389391
# Get the color to use for this system
390-
if marker_color is None:
392+
if user_color is None:
391393
color = color_cycle[(color_offset + idx) % len(color_cycle)]
392394
else:
393-
color = marker_color
395+
color = user_color
394396

395397
# Plot the locations of the poles and zeros
396398
if len(poles) > 0:

control/rlocus.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def root_locus_plot(
130130
initial_gain : float, optional
131131
Mark the point on the root locus diagram corresponding to the
132132
given gain.
133+
color : matplotlib color spec, optional
134+
Specify the color of the markers and lines.
133135
134136
Returns
135137
-------

0 commit comments

Comments
 (0)