Merge pull request #953 from murrayrm/pzmap_plots-22Jul2023 · python-control/python-control@eb0f3f9
1-import numpy as np
2-from numpy import cos, sin, sqrt, linspace, pi, exp
1+# grid.py - code to add gridlines to root locus and pole-zero diagrams
2+#
3+# This code generates grids for pole-zero diagrams (including root locus
4+# diagrams). Rather than just draw a grid in place, it uses the AxisArtist
5+# package to generate a custom grid that will scale with the figure.
6+#
7+38import matplotlib.pyplot as plt
4-from mpl_toolkits.axisartist import SubplotHost
5-from mpl_toolkits.axisartist.grid_helper_curvelinear \
6-import GridHelperCurveLinear
79import mpl_toolkits.axisartist.angle_helper as angle_helper
10+import numpy as np
811from matplotlib.projections import PolarAxes
912from matplotlib.transforms import Affine2D
13+from mpl_toolkits.axisartist import SubplotHost
14+from mpl_toolkits.axisartist.grid_helper_curvelinear import \
15+GridHelperCurveLinear
16+from numpy import cos, exp, linspace, pi, sin, sqrt
17+18+from .iosys import isdtime
101911201221class FormatterDMS(object):
@@ -65,14 +74,15 @@ def __call__(self, transform_xy, x1, y1, x2, y2):
6574return lon_min, lon_max, lat_min, lat_max
6675677668-def sgrid():
77+def sgrid(scaling=None):
6978# From matplotlib demos:
7079# https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html
7180# https://matplotlib.org/gallery/axisartist/demo_floating_axis.html
72817382# PolarAxes.PolarTransform takes radian. However, we want our coordinate
74-# system in degree
83+# system in degrees
7584tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
85+7686# polar projection, which involves cycle, and also has limits in
7787# its coordinates, needs a special method to find the extremes
7888# (min, max of the coordinate within the view).
@@ -89,6 +99,7 @@ def sgrid():
8999tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1,
90100tick_formatter1=tick_formatter1)
91101102+# Set up an axes with a specialized grid helper
92103fig = plt.gcf()
93104ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
94105@@ -97,15 +108,20 @@ def sgrid():
97108ax.axis[:].major_ticklabels.set_visible(visible)
98109ax.axis[:].major_ticks.set_visible(False)
99110ax.axis[:].invert_ticklabel_direction()
111+ax.axis[:].major_ticklabels.set_color('gray')
100112113+# Set up internal tickmarks and labels along the real/imag axes
101114ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180)
102115axis.set_ticklabel_direction("-")
103116axis.label.set_visible(False)
117+104118ax.axis["wnxpos"] = axis = ax.new_floating_axis(0, 0)
105119axis.label.set_visible(False)
120+106121ax.axis["wnypos"] = axis = ax.new_floating_axis(0, 90)
107122axis.label.set_visible(False)
108-axis.set_axis_direction("left")
123+axis.set_axis_direction("right")
124+109125ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270)
110126axis.label.set_visible(False)
111127axis.set_axis_direction("left")
@@ -119,43 +135,41 @@ def sgrid():
119135ax.axis["bottom"].get_helper().nth_coord_ticks = 0
120136121137fig.add_subplot(ax)
122-123-# RECTANGULAR X Y AXES WITH SCALE
124-# par2 = ax.twiny()
125-# par2.axis["top"].toggle(all=False)
126-# par2.axis["right"].toggle(all=False)
127-# new_fixed_axis = par2.get_grid_helper().new_fixed_axis
128-# par2.axis["left"] = new_fixed_axis(loc="left",
129-# axes=par2,
130-# offset=(0, 0))
131-# par2.axis["bottom"] = new_fixed_axis(loc="bottom",
132-# axes=par2,
133-# offset=(0, 0))
134-# FINISH RECTANGULAR
135-136138ax.grid(True, zorder=0, linestyle='dotted')
137139138-_final_setup(ax)
140+_final_setup(ax, scaling=scaling)
139141return ax, fig
140142141143142-def _final_setup(ax):
144+# Utility function used by all grid code
145+def _final_setup(ax, scaling=None):
143146ax.set_xlabel('Real')
144147ax.set_ylabel('Imaginary')
145-ax.axhline(y=0, color='black', lw=1)
146-ax.axvline(x=0, color='black', lw=1)
147-plt.axis('equal')
148+ax.axhline(y=0, color='black', lw=0.25)
149+ax.axvline(x=0, color='black', lw=0.25)
148150151+# Set up the scaling for the axes
152+scaling = 'equal' if scaling is None else scaling
153+plt.axis(scaling)
149154150-def nogrid():
151-f = plt.gcf()
152-ax = plt.axes()
153155154-_final_setup(ax)
155-return ax, f
156+# If not grid is given, at least separate stable/unstable regions
157+def nogrid(dt=None, ax=None, scaling=None):
158+fig = plt.gcf()
159+if ax is None:
160+ax = fig.gca()
161+162+# Draw the unit circle for discrete time systems
163+if isdtime(dt=dt, strict=True):
164+s = np.linspace(0, 2*pi, 100)
165+ax.plot(np.cos(s), np.sin(s), 'k--', lw=0.5, dashes=(5, 5))
156166167+_final_setup(ax, scaling=scaling)
168+return ax, fig
157169158-def zgrid(zetas=None, wns=None, ax=None):
170+# Grid for discrete time system (drawn, not rendered by AxisArtist)
171+# TODO (at some point): think about using customized grid generator?
172+def zgrid(zetas=None, wns=None, ax=None, scaling=None):
159173"""Draws discrete damping and frequency grid"""
160174161175fig = plt.gcf()
@@ -206,5 +220,9 @@ def zgrid(zetas=None, wns=None, ax=None):
206220ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y),
207221xytext=(an_x, an_y), size=9)
208222209-_final_setup(ax)
223+# Set default axes to allow some room around the unit circle
224+ax.set_xlim([-1.1, 1.1])
225+ax.set_ylim([-1.1, 1.1])
226+227+_final_setup(ax, scaling=scaling)
210228return ax, fig