GeezerVison is showing github.com — recalculate loci for sisotool and rlocus on axis scaling · python-control/python-control@b588045
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: control/pzmap.py +40Lines changed: 40 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -124,6 +124,17 @@ def plot(self, *args, **kwargs): | 124 | 124 | """ | 125 | 125 | return pole_zero_plot(self, *args, **kwargs) | 126 | 126 |
| | 127 | + def replot(self, cplt: ControlPlot): | | 128 | + """Update the pole/zero loci of an existing plot. | | 129 | +
| | 130 | + Parameters | | 131 | + ---------- | | 132 | + cplt: ControlPlot | | 133 | + Graphics handles of the existing plot. | | 134 | + """ | | 135 | + pole_zero_replot(self, cplt) | | 136 | + | | 137 | + | 127 | 138 |
| 128 | 139 | # Pole/zero map | 129 | 140 | def pole_zero_map(sysdata): | @@ -513,6 +524,35 @@ def _click_dispatcher(event): | 513 | 524 | return ControlPlot(out, ax, fig, legend=legend) | 514 | 525 |
| 515 | 526 |
| | 527 | +def pole_zero_replot(pzmap_responses, cplt): | | 528 | + """Update the loci of a plot after zooming/panning. | | 529 | +
| | 530 | + Parameters | | 531 | + ---------- | | 532 | + pzmap_responses : PoleZeroMap list | | 533 | + Responses to update. | | 534 | + cplt : ControlPlot | | 535 | + Collection of plot handles. | | 536 | + """ | | 537 | + | | 538 | + for idx, response in enumerate(pzmap_responses): | | 539 | + | | 540 | + # remove the old data | | 541 | + for l in cplt.lines[idx, 2]: | | 542 | + l.set_data([], []) | | 543 | + | | 544 | + # update the line data | | 545 | + if response.loci is not None: | | 546 | + | | 547 | + for il, locus in enumerate(response.loci.transpose()): | | 548 | + try: | | 549 | + cplt.lines[idx,2][il].set_data(real(locus), imag(locus)) | | 550 | + except IndexError: | | 551 | + # not expected, but more lines apparently needed | | 552 | + cplt.lines[idx,2].append(cplt.ax[0,0].plot( | | 553 | + real(locus), imag(locus))) | | 554 | + | | 555 | + | 516 | 556 | # Utility function to find gain corresponding to a click event | 517 | 557 | def _find_root_locus_gain(event, sys, ax): | 518 | 558 | # Get the current axis limits to set various thresholds | |
Collapse file Copy file name to clipboardExpand all lines: control/rlocus.py +46-3Lines changed: 46 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 33 | 33 |
| 34 | 34 |
| 35 | 35 | # Root locus map | 36 | | -def root_locus_map(sysdata, gains=None): | | 36 | +def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): | 37 | 37 | """Compute the root locus map for an LTI system. | 38 | 38 |
| 39 | 39 | Calculate the root locus by finding the roots of 1 + k * G(s) where G | @@ -46,6 +46,10 @@ def root_locus_map(sysdata, gains=None): | 46 | 46 | gains : array_like, optional | 47 | 47 | Gains to use in computing plot of closed-loop poles. If not given, | 48 | 48 | gains are chosen to include the main features of the root locus map. | | 49 | + xlim : tuple or list, optional | | 50 | + Set limits of x axis (see `matplotlib.axes.Axes.set_xlim`). | | 51 | + ylim : tuple or list, optional | | 52 | + Set limits of y axis (see `matplotlib.axes.Axes.set_ylim`). | 49 | 53 |
| 50 | 54 | Returns | 51 | 55 | ------- | @@ -75,7 +79,7 @@ def root_locus_map(sysdata, gains=None): | 75 | 79 | nump, denp = _systopoly1d(sys[0, 0]) | 76 | 80 |
| 77 | 81 | if gains is None: | 78 | | - kvect, root_array, _, _ = _default_gains(nump, denp, None, None) | | 82 | + kvect, root_array, _, _ = _default_gains(nump, denp, xlim, ylim) | 79 | 83 | else: | 80 | 84 | kvect = np.atleast_1d(gains) | 81 | 85 | root_array = _RLFindRoots(nump, denp, kvect) | @@ -205,13 +209,52 @@ def root_locus_plot( | 205 | 209 | # Plot the root loci | 206 | 210 | cplt = responses.plot(grid=grid, **kwargs) | 207 | 211 |
| | 212 | + # Add a reaction to axis scale changes, if given LTI systems, and | | 213 | + # there is no set of pre-defined gains | | 214 | + if gains is None: | | 215 | + add_loci_recalculate(sysdata, cplt, cplt.axes[0,0]) | | 216 | + | 208 | 217 | # Legacy processing: return locations of poles and zeros as a tuple | 209 | 218 | if plot is True: | 210 | 219 | return responses.loci, responses.gains | 211 | 220 |
| 212 | 221 | return ControlPlot(cplt.lines, cplt.axes, cplt.figure) | 213 | 222 |
| 214 | 223 |
| | 224 | +def add_loci_recalculate(sysdata, cplt, axis): | | 225 | + """Add a callback to re-calculate the loci data fitting a zoom action. | | 226 | +
| | 227 | + Parameters | | 228 | + ---------- | | 229 | + sysdata: LTI object or list | | 230 | + Linear input/output systems (SISO only, for now). | | 231 | + cplt: ControlPlot | | 232 | + Collection of plot handles. | | 233 | + axis: matplotlib.axes.Axis | | 234 | + Axis on which callbacks are installed. | | 235 | + """ | | 236 | + | | 237 | + # if LTI, treat everything as a list of lti | | 238 | + if isinstance(sysdata, LTI): | | 239 | + sysdata = [sysdata] | | 240 | + | | 241 | + # check that we can actually recalculate the loci | | 242 | + if isinstance(sysdata, list) and all( | | 243 | + [isinstance(sys, LTI) for sys in sysdata]): | | 244 | + | | 245 | + # callback function for axis change (zoom, pan) events | | 246 | + # captures the sysdata object and cplt | | 247 | + def _zoom_adapter(_ax): | | 248 | + newresp = root_locus_map(sysdata, None, | | 249 | + _ax.get_xlim(), | | 250 | + _ax.get_ylim()) | | 251 | + newresp.replot(cplt) | | 252 | + | | 253 | + # connect the callback to axis changes | | 254 | + axis.callbacks.connect('xlim_changed', _zoom_adapter) | | 255 | + axis.callbacks.connect('ylim_changed', _zoom_adapter) | | 256 | + | | 257 | + | 215 | 258 | def _default_gains(num, den, xlim, ylim): | 216 | 259 | """Unsupervised gains calculation for root locus plot. | 217 | 260 |
| @@ -288,7 +331,7 @@ def _default_gains(num, den, xlim, ylim): | 288 | 331 | # Root locus is on imaginary axis (rare), use just y distance | 289 | 332 | tolerance = y_tolerance | 290 | 333 | elif y_tolerance == 0: | 291 | | - # Root locus is on imaginary axis (common), use just x distance | | 334 | + # Root locus is on real axis (common), use just x distance | 292 | 335 | tolerance = x_tolerance | 293 | 336 | else: | 294 | 337 | tolerance = np.min([x_tolerance, y_tolerance]) | |
Collapse file Copy file name to clipboardExpand all lines: control/sisotool.py +9-6Lines changed: 9 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 22 | 22 | from .statesp import ss, summing_junction | 23 | 23 | from .timeresp import step_response | 24 | 24 | from .xferfcn import tf | | 25 | +from .rlocus import add_loci_recalculate | 25 | 26 |
| 26 | 27 | _sisotool_defaults = { | 27 | 28 | 'sisotool.initial_gain': 1 | @@ -105,7 +106,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, | 105 | 106 | fig = plt.gcf() | 106 | 107 | if fig.canvas.manager.get_window_title() != 'Sisotool': | 107 | 108 | plt.close(fig) | 108 | | - fig,axes = plt.subplots(2, 2) | | 109 | + fig, axes = plt.subplots(2, 2) | 109 | 110 | fig.canvas.manager.set_window_title('Sisotool') | 110 | 111 | else: | 111 | 112 | axes = np.array(fig.get_axes()).reshape(2, 2) | @@ -137,15 +138,18 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, | 137 | 138 | # sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus, | 138 | 139 | # ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, | 139 | 140 | # ax=fig.axes[1]) | 140 | | - ax_rlocus = fig.axes[1] | 141 | | - root_locus_map(sys[0, 0]).plot( | | 141 | + ax_rlocus = axes[0,1] # fig.axes[1] | | 142 | + cplt = root_locus_map(sys[0, 0]).plot( | 142 | 143 | xlim=xlim_rlocus, ylim=ylim_rlocus, | 143 | 144 | initial_gain=initial_gain, ax=ax_rlocus) | 144 | 145 | if rlocus_grid is False: | 145 | 146 | # Need to generate grid manually, since root_locus_plot() won't | 146 | 147 | from .grid import nogrid | 147 | 148 | nogrid(sys.dt, ax=ax_rlocus) | 148 | 149 |
| | 150 | + # install a zoom callback on the root-locus axis | | 151 | + add_loci_recalculate(sys, cplt, ax_rlocus) | | 152 | + | 149 | 153 | # Reset the button release callback so that we can update all plots | 150 | 154 | fig.canvas.mpl_connect( | 151 | 155 | 'button_release_event', partial( | @@ -155,9 +159,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, | 155 | 159 |
| 156 | 160 | def _click_dispatcher(event, sys, ax, bode_plot_params, tvect): | 157 | 161 | # Zoom handled by specialized callback in rlocus, only handle gain plot | 158 | | - if event.inaxes == ax.axes and \ | 159 | | - plt.get_current_fig_manager().toolbar.mode not in \ | 160 | | - {'zoom rect', 'pan/zoom'}: | | 162 | + if event.inaxes == ax.axes: | | 163 | + | 161 | 164 | fig = ax.figure | 162 | 165 |
| 163 | 166 | # if a point is clicked on the rootlocus plot visually emphasize it | |
|