8000 add unit tests + docuementation for subplots · toaster-code/python-control@4ec3612 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4ec3612

Browse files
committed
add unit tests + docuementation for subplots
1 parent c3707d3 commit 4ec3612

File tree

7 files changed

+133
-10
lines changed

7 files changed

+133
-10
lines changed

control/pzmap.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,16 @@ def pole_zero_plot(
265265
266266
Notes
267267
-----
268-
By default, the pzmap function calls matplotlib.pyplot.axis('equal'),
269-
which means that trying to reset the axis limits may not behave as
270-
expected. To change the axis limits, use the `scaling` keyword of use
271-
matplotlib.pyplot.gca().axis('auto') and then set the axis limits to
272-
the desired values.
268+
1. By default, the pzmap function calls matplotlib.pyplot.axis('equal'),
269+
which means that trying to reset the axis limits may not behave as
270+
expected. To change the axis limits, use the `scaling` keyword of
271+
use matplotlib.pyplot.gca().axis('auto') and then set the axis
272+
limits to the desired values.
273+
274+
2. Pole/zero plts that use the continuous time omega-damping grid do
275+
not work with the ``ax`` keyword argument, due to the way that axes
276+
grids are implemented. The ``grid`` argument must be set to
277+
``False`` or `'empty'`` when using the ``ax`` keyword argument.
273278
274279
"""
275280
# Get parameter values

control/tests/ctrlplot_test.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,3 +782,64 @@ def test_ControlPlot_init():
782782
assert np.all(cplt_raw.axes == cplt.axes)
783783
assert cplt_raw.figure == cplt.figure
784784

785+
def test_pole_zero_subplots(savefig=False):
786+
ax_array = ct.pole_zero_subplots(2, 1, grid=[True, False])
787+
sys1 = ct.tf([1, 2], [1, 2, 3], name='sys1')
788+
sys2 = ct.tf([1, 0.2], [1, 1, 3, 1, 1], name='sys2')
789+
ct.root_locus_plot([sys1, sys2], ax=ax_array[0, 0])
790+
cplt = ct.root_locus_plot([sys1, sys2], ax=ax_array[1, 0])
791+
with pytest.warns(UserWarning, match="Tight layout not applied"):
792+
cplt.set_plot_title("Root locus plots (w/ specified axes)")
793+
if savefig:
794+
plt.savefig("ctrlplot-pole_zero_subplots.png")
795+
796+
if __name__ == "__main__":
797+
#
798+
# Interactive mode: generate plots for manual viewing
799+
#
800+
# Running this script in python (or better ipython) will show a
801+
# collection of figures that should all look OK on the screeen.
802+
#
803+
804+
# In interactive mode, turn on ipython interactive graphics
805+
plt.ion()
806+
807+
# Start by clearing existing figures
808+
plt.close('all')
809+
810+
#
811+
# Combination plot
812+
#
813+
814+
P = ct.tf([0.02], [1, 0.1, 0.01]) # servomechanism
815+
C1 = ct.tf([1, 1], [1, 0]) # unstable
816+
L1 = P * C1
817+
C2 = ct.tf([1, 0.05], [1, 0]) # stable
818+
L2 = P * C2
819+
820+
plt.rcParams.update(ct.rcParams)
821+
fig = plt.figure(figsize=[7, 4])
822+
ax_mag = fig.add_subplot(2, 2, 1)
823+
ax_phase = fig.add_subplot(2, 2, 3)
824+
ax_nyquist = fig.add_subplot(1, 2, 2)
825+
826+
ct.bode_plot(
827+
[L1, L2], ax=[ax_mag, ax_phase],
828+
label=["$L_1$ (unstable)", "$L_2$ (unstable)"],
829+
show_legend=False)
830+
ax_mag.set_title("Bode plot for $L_1$, $L_2$")
831+
ax_mag.tick_params(labelbottom=False)
832+
fig.align_labels()
833+
834+
ct.nyquist_plot(L1, ax=ax_nyquist, label="$L_1$ (unstable)")
835+
ct.nyquist_plot(
836+
L2, ax=ax_nyquist, label="$L_2$ (stable)",
837+
max_curve_magnitude=22, legend_loc='upper right')
838+
ax_nyquist.set_title("Nyquist plot for $L_1$, $L_2$")
839+
840+
fig.suptitle("Loop analysis for servomechanism control design")
841+
plt.tight_layout()
842+
plt.savefig('ctrlplot-servomech.png')
843+
844+
plt.figure()
845+
test_pole_zero_subplots(savefig=True)

doc/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ help:
1717
# Rules to create figures
1818
FIGS = classes.pdf timeplot-mimo_step-default.png \
1919
freqplot-siso_bode-default.png rlocus-siso_ctime-default.png \
20-
phaseplot-dampedosc-default.png
20+
phaseplot-dampedosc-default.png ctrlplot-servomech.png
2121
classes.pdf: classes.fig
2222
fig2dev -Lpdf $< $@
2323

@@ -33,6 +33,9 @@ rlocus-siso_ctime-default.png: ../control/tests/rlocus_test.py
3333
phaseplot-dampedosc-default.png: ../control/tests/phaseplot_test.py
3434
PYTHONPATH=.. python $<
3535

36+
ctrlplot-servomech.png: ../control/tests/ctrlplot_test.py
37+
PYTHONPATH=.. python $<
38+
3639
# Catch-all target: route all unknown targets to Sphinx using the new
3740
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
3841
html pdf clean doctest: Makefile $(FIGS)

doc/ctrlplot-pole_zero_subplots.png

65.6 KB
Loading

doc/ctrlplot-servomech.png

62 KB
Loading

doc/phaseplots.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

doc/plotting.rst

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,15 +431,15 @@ various ways. The following general rules apply:
431431

432432
* If a plotting function is called multiple times with data that generate
433433
control plots with the same shape for the array of subplots, the new data
434-
will be overlayed with the old data, with a change in color(s) for the
434+
will be overlaid with the old data, with a change in color(s) for the
435435
new data (chosen from the standard matplotlib color cycle). If not
436436
overridden, the plot title and legends will be updated to reflect all
437437
data shown on the plot.
438438

439439
* If a plotting function is called and the shape for the array of subplots
440440
does not match the currently displayed plot, a new figure is created.
441441
Note that only the shape is checked, so if two different types of
442-
plotting commands that generate the same shape of suplots are called
442+
plotting commands that generate the same shape of subplots are called
443443
sequentially, the :func:`matplotlib.pyplot.figure` command should be used
444444
to explicitly create a new figure.
445445

@@ -485,7 +485,7 @@ various ways. The following general rules apply:
485485
the ``legend_loc`` keyword argument is set to a string or integer, it
486486
will set the position of the legend as described in the
487487
:func:`matplotlib.legend`` documentation. Finally, ``legend_map`` can be
488-
set to an` array that matches the shape of the suplots, with each item
488+
set to an` array that matches the shape of the subplots, with each item
489489
being a string indicating the location of the legend for that axes (or
490490
``None`` for no legend).
491491

@@ -530,6 +530,61 @@ various ways. The following general rules apply:
530530

531531
The plot title is only generated if ``ax`` is ``None``.
532532

533+
The following code illustrates the use of some of these customization
534+
features::
535+
536+
P = ct.tf([0.02], [1, 0.1, 0.01]) # servomechanism
537+
C1 = ct.tf([1, 1], [1, 0]) # unstable
538+
L1 = P * C1
539+
C2 = ct.tf([1, 0.05], [1, 0]) # stable
540+
L2 = P * C2
541+
542+
plt.rcParams.update(ct.rcParams)
543+
fig = plt.figure(figsize=[7, 4])
544+
ax_mag = fig.add_subplot(2, 2, 1)
545+
ax_phase = fig.add_subplot(2, 2, 3)
546+
ax_nyquist = fig.add_subplot(1, 2, 2)
547+
548+
ct.bode_plot(
549+
[L1, L2], ax=[ax_mag, ax_phase],
550+
label=["$L_1$ (unstable)", "$L_2$ (unstable)"],
551+
show_legend=False)
552+
ax_mag.set_title("Bode plot for $L_1$, $L_2$")
553+
ax_mag.tick_params(labelbottom=False)
554+
fig.align_labels()
555+
556+
ct.nyquist_plot(L1, ax=ax_nyquist, label="$L_1$ (unstable)")
557+
ct.nyquist_plot(
558+
L2, ax=ax_nyquist, label="$L_2$ (stable)",
559+
max_curve_magnitude=22, legend_loc='upper right')
560+
ax_nyquist.set_title("Nyquist plot for $L_1$, $L_2$")
561+
562+
fig.suptitle("Loop analysis for servomechanism control design")
563+
plt.tight_layout()
564+
565+
.. image:: ctrlplot-servomech.png
566+
567+
As this example illustrates, python-control plotting functions and
568+
Matplotlib plotting functions can generally be intermixed. One type of
569+
plot for which this does not currently work is pole/zero plots with a
570+
continuous time omega-damping grid (including root locus diagrams), due to
571+
the way that axes grids are implemented. As a workaround, the
572+
:func:`~control.pole_zero_subplots` command can be used to create an array
573+
of subplots with different grid types, as illustrated in the following
574+
example::
575+
576+
ax_array = ct.pole_zero_subplots(2, 1, grid=[True, False])
577+
sys1 = ct.tf([1, 2], [1, 2, 3], name='sys1')
578+
sys2 = ct.tf([1, 0.2], [1, 1, 3, 1, 1], name='sys2')
579+
ct.root_locus_plot([sys1, sys2], ax=ax_array[0, 0])
580+
cplt = ct.root_locus_plot([sys1, sys2], ax=ax_array[1, 0])
581+
cplt.set_plot_title("Root locus plots (w/ specified axes)")
582+
583+
.. image:: ctrlplot-pole_zero_subplots.png
584+
585+
Alternatively, turning off the omega-damping grid (using ``grid=False`` or
586+
``grid='empty'``) allows use of Matplotlib layout commands.
587+
533588

534589
Response and plotting functions
535590
===============================

0 commit comments

Comments
 (0)
0