• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

python-control / python-control / 13107996232

03 Feb 2025 06:53AM UTC coverage: 94.731% (+0.02%) from 94.709%
13107996232

push

github

web-flow
Merge pull request #1094 from murrayrm/userguide-22Dec2024

Updated user documentation (User Guide, Reference Manual)

9673 of 10211 relevant lines covered (94.73%)

8.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.74
control/grid.py
1
# grid.py - code to add gridlines to root locus and pole-zero diagrams
2

3
"""Functions to add gridlines to root locus and pole-zero diagrams.
4

5
This code generates grids for pole-zero diagrams (including root locus
6
diagrams).  Rather than just draw a grid in place, it uses the
7
AxisArtist package to generate a custom grid that will scale with the
8
figure.
9

10
"""
11

12
import matplotlib.pyplot as plt
9✔
13
import mpl_toolkits.axisartist.angle_helper as angle_helper
9✔
14
import numpy as np
9✔
15
from matplotlib.projections import PolarAxes
9✔
16
from matplotlib.transforms import Affine2D
9✔
17
from mpl_toolkits.axisartist import SubplotHost
9✔
18
from mpl_toolkits.axisartist.grid_helper_curvelinear import \
9✔
19
    GridHelperCurveLinear
20
from numpy import cos, exp, linspace, pi, sin, sqrt
9✔
21

22
from .iosys import isdtime
9✔
23

24

25
class FormatterDMS():
9✔
26
    """Transforms angle ticks to damping ratios."""
27
    def __call__(self, direction, factor, values):
9✔
28
        angles_deg = np.asarray(values)/factor
9✔
29
        damping_ratios = np.cos((180-angles_deg) * np.pi/180)
9✔
30
        ret = ["%.2f" % val for val in damping_ratios]
9✔
31
        return ret
9✔
32

33

34
class ModifiedExtremeFinderCycle(angle_helper.ExtremeFinderCycle):
9✔
35
    """Changed to allow only left hand-side polar grid.
36

37
    https://matplotlib.org/_modules/mpl_toolkits/axisartist/angle_helper.html#ExtremeFinderCycle.__call__
38
    """
39
    def __call__(self, transform_xy, x1, y1, x2, y2):
9✔
40
        x, y = np.meshgrid(
9✔
41
            np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
42
        lon, lat = transform_xy(np.ravel(x), np.ravel(y))
9✔
43

44
        with np.errstate(invalid='ignore'):
9✔
45
            if self.lon_cycle is not None:
9✔
46
                lon0 = np.nanmin(lon)
9✔
47
                # Changed from 180 to 360 to be able to span only
48
                # 90-270 (left hand side)
49
                lon -= 360. * ((lon - lon0) > 360.)
9✔
50
            if self.lat_cycle is not None:  # pragma: no cover
9✔
51
                lat0 = np.nanmin(lat)
×
52
                lat -= 360. * ((lat - lat0) > 180.)
×
53

54
        lon_min, lon_max = np.nanmin(lon), np.nanmax(lon)
9✔
55
        lat_min, lat_max = np.nanmin(lat), np.nanmax(lat)
9✔
56

57
        lon_min, lon_max, lat_min, lat_max = \
9✔
58
            self._add_pad(lon_min, lon_max, lat_min, lat_max)
59

60
        # check cycle
61
        if self.lon_cycle:
9✔
62
            lon_max = min(lon_max, lon_min + self.lon_cycle)
9✔
63
        if self.lat_cycle:  # pragma: no cover
9✔
64
            lat_max = min(lat_max, lat_min + self.lat_cycle)
×
65

66
        if self.lon_minmax is not None:
9✔
67
            min0 = self.lon_minmax[0]
9✔
68
            lon_min = max(min0, lon_min)
9✔
69
            max0 = self.lon_minmax[1]
9✔
70
            lon_max = min(max0, lon_max)
9✔
71

72
        if self.lat_minmax is not None:
9✔
73
            min0 = self.lat_minmax[0]
9✔
74
            lat_min = max(min0, lat_min)
9✔
75
            max0 = self.lat_minmax[1]
9✔
76
            lat_max = min(max0, lat_max)
9✔
77

78
        return lon_min, lon_max, lat_min, lat_max
9✔
79

80

81
def sgrid(subplot=(1, 1, 1), scaling=None):
9✔
82
    # From matplotlib demos:
83
    # https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html
84
    # https://matplotlib.org/gallery/axisartist/demo_floating_axis.html
85

86
    # PolarAxes.PolarTransform takes radian. However, we want our coordinate
87
    # system in degrees
88
    tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
9✔
89

90
    # polar projection, which involves cycle, and also has limits in
91
    # its coordinates, needs a special method to find the extremes
92
    # (min, max of the coordinate within the view).
93

94
    # 20, 20 : number of sampling points along x, y direction
95
    sampling_points = 20
9✔
96
    extreme_finder = ModifiedExtremeFinderCycle(
9✔
97
        sampling_points, sampling_points, lon_cycle=360, lat_cycle=None,
98
        lon_minmax=(90, 270), lat_minmax=(0, np.inf),)
99

100
    grid_locator1 = angle_helper.LocatorDMS(15)
9✔
101
    tick_formatter1 = FormatterDMS()
9✔
102
    grid_helper = GridHelperCurveLinear(
9✔
103
        tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1,
104
        tick_formatter1=tick_formatter1)
105

106
    # Set up an axes with a specialized grid helper
107
    fig = plt.gcf()
9✔
108
    ax = SubplotHost(fig, *subplot, grid_helper=grid_helper)
9✔
109

110
    # make ticklabels of right invisible, and top axis visible.
111
    ax.axis[:].major_ticklabels.set_visible(True)
9✔
112
    ax.axis[:].major_ticks.set_visible(False)
9✔
113
    ax.axis[:].invert_ticklabel_direction()
9✔
114
    ax.axis[:].major_ticklabels.set_color('gray')
9✔
115

116
    # Set up internal tickmarks and labels along the real/imag axes
117
    ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180)
9✔
118
    axis.set_ticklabel_direction("-")
9✔
119
    axis.label.set_visible(False)
9✔
120

121
    ax.axis["wnxpos"] = axis = ax.new_floating_axis(0, 0)
9✔
122
    axis.label.set_visible(False)
9✔
123

124
    ax.axis["wnypos"] = axis = ax.new_floating_axis(0, 90)
9✔
125
    axis.label.set_visible(False)
9✔
126
    axis.set_axis_direction("right")
9✔
127

128
    ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270)
9✔
129
    axis.label.set_visible(False)
9✔
130
    axis.set_axis_direction("left")
9✔
131
    axis.invert_ticklabel_direction()
9✔
132
    axis.set_ticklabel_direction("-")
9✔
133

134
    # let left axis shows ticklabels for 1st coordinate (angle)
135
    ax.axis["left"].get_helper().nth_coord_ticks = 0
9✔
136
    ax.axis["right"].get_helper().nth_coord_ticks = 0
9✔
137
    ax.axis["left"].get_helper().nth_coord_ticks = 0
9✔
138
    ax.axis["bottom"].get_helper().nth_coord_ticks = 0
9✔
139

140
    fig.add_subplot(ax)
9✔
141
    ax.grid(True, zorder=0, linestyle='dotted')
9✔
142

143
    _final_setup(ax, scaling=scaling)
9✔
144
    return ax, fig
9✔
145

146

147
# If not grid is given, at least separate stable/unstable regions
148
def nogrid(dt=None, ax=None, scaling=None):
9✔
149
    fig = plt.gcf()
9✔
150
    if ax is None:
9✔
151
        ax = fig.gca()
9✔
152

153
    # Draw the unit circle for discrete-time systems
154
    if isdtime(dt=dt, strict=True):
9✔
155
        s = np.linspace(0, 2*pi, 100)
9✔
156
        ax.plot(np.cos(s), np.sin(s), 'k--', lw=0.5, dashes=(5, 5))
9✔
157

158
    _final_setup(ax, scaling=scaling)
9✔
159
    return ax, fig
9✔
160

161
# Grid for discrete-time system (drawn, not rendered by AxisArtist)
162
# TODO (at some point): think about using customized grid generator?
163
def zgrid(zetas=None, wns=None, ax=None, scaling=None):
9✔
164
    """Draws discrete damping and frequency grid"""
165

166
    fig = plt.gcf()
9✔
167
    if ax is None:
9✔
168
        ax = fig.gca()
9✔
169

170
    # Constant damping lines
171
    if zetas is None:
9✔
172
        zetas = linspace(0, 0.9, 10)
9✔
173
    for zeta in zetas:
9✔
174
        # Calculate in polar coordinates
175
        factor = zeta/sqrt(1-zeta**2)
9✔
176
        x = linspace(0, sqrt(1-zeta**2), 200)
9✔
177
        ang = pi*x
9✔
178
        mag = exp(-pi*factor*x)
9✔
179
        # Draw upper part in rectangular coordinates
180
        xret = mag*cos(ang)
9✔
181
        yret = mag*sin(ang)
9✔
182
        ax.plot(xret, yret, ':', color='grey', lw=0.75)
9✔
183
        # Draw lower part in rectangular coordinates
184
        xret = mag*cos(-ang)
9✔
185
        yret = mag*sin(-ang)
9✔
186
        ax.plot(xret, yret, ':', color='grey', lw=0.75)
9✔
187
        # Annotation
188
        an_i = int(len(xret)/2.5)
9✔
189
        an_x = xret[an_i]
9✔
190
        an_y = yret[an_i]
9✔
191
        ax.annotate(str(round(zeta, 2)), xy=(an_x, an_y),
9✔
192
                    xytext=(an_x, an_y), size=7)
193

194
    # Constant natural frequency lines
195
    if wns is None:
9✔
196
        wns = linspace(0, 1, 10)
9✔
197
    for a in wns:
9✔
198
        # Calculate in polar coordinates
199
        x = linspace(-pi/2, pi/2, 200)
9✔
200
        ang = pi*a*sin(x)
9✔
201
        mag = exp(-pi*a*cos(x))
9✔
202
        # Draw in rectangular coordinates
203
        xret = mag*cos(ang)
9✔
204
        yret = mag*sin(ang)
9✔
205
        ax.plot(xret, yret, ':', color='grey', lw=0.75)
9✔
206
        # Annotation
207
        an_i = -1
9✔
208
        an_x = xret[an_i]
9✔
209
        an_y = yret[an_i]
9✔
210
        num = '{:1.1f}'.format(a)
9✔
211
        ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y),
9✔
212
                    xytext=(an_x, an_y), size=9)
213

214
    # Set default axes to allow some room around the unit circle
215
    ax.set_xlim([-1.1, 1.1])
9✔
216
    ax.set_ylim([-1.1, 1.1])
9✔
217

218
    _final_setup(ax, scaling=scaling)
9✔
219
    return ax, fig
9✔
220

221

222
# Utility function used by all grid code
223
def _final_setup(ax, scaling=None):
9✔
224
    ax.set_xlabel('Real')
9✔
225
    ax.set_ylabel('Imaginary')
9✔
226
    ax.axhline(y=0, color='black', lw=0.25)
9✔
227
    ax.axvline(x=0, color='black', lw=0.25)
9✔
228

229
    # Set up the scaling for the axes
230
    scaling = 'equal' if scaling is None else scaling
9✔
231
    plt.axis(scaling)
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc