From b588045618729c79db8227f55ff5af615bcf76a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20van=20Paassen?= Date: Thu, 5 Jun 2025 00:22:56 +0200 Subject: [PATCH] recalculate loci for sisotool and rlocus on axis scaling --- control/pzmap.py | 40 ++++++++++++++++++++++++++++++++++++ control/rlocus.py | 49 ++++++++++++++++++++++++++++++++++++++++++--- control/sisotool.py | 15 ++++++++------ 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index 42ba8e087..f1e3c6545 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -124,6 +124,17 @@ def plot(self, *args, **kwargs): """ return pole_zero_plot(self, *args, **kwargs) + def replot(self, cplt: ControlPlot): + """Update the pole/zero loci of an existing plot. + + Parameters + ---------- + cplt: ControlPlot + Graphics handles of the existing plot. + """ + pole_zero_replot(self, cplt) + + # Pole/zero map def pole_zero_map(sysdata): @@ -513,6 +524,35 @@ def _click_dispatcher(event): return ControlPlot(out, ax, fig, legend=legend) +def pole_zero_replot(pzmap_responses, cplt): + """Update the loci of a plot after zooming/panning. + + Parameters + ---------- + pzmap_responses : PoleZeroMap list + Responses to update. + cplt : ControlPlot + Collection of plot handles. + """ + + for idx, response in enumerate(pzmap_responses): + + # remove the old data + for l in cplt.lines[idx, 2]: + l.set_data([], []) + + # update the line data + if response.loci is not None: + + for il, locus in enumerate(response.loci.transpose()): + try: + cplt.lines[idx,2][il].set_data(real(locus), imag(locus)) + except IndexError: + # not expected, but more lines apparently needed + cplt.lines[idx,2].append(cplt.ax[0,0].plot( + real(locus), imag(locus))) + + # Utility function to find gain corresponding to a click event def _find_root_locus_gain(event, sys, ax): # Get the current axis limits to set various thresholds diff --git a/control/rlocus.py b/control/rlocus.py index c4ef8b40e..b7344e31d 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -33,7 +33,7 @@ # Root locus map -def root_locus_map(sysdata, gains=None): +def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): """Compute the root locus map for an LTI system. 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): gains : array_like, optional Gains to use in computing plot of closed-loop poles. If not given, gains are chosen to include the main features of the root locus map. + xlim : tuple or list, optional + Set limits of x axis (see `matplotlib.axes.Axes.set_xlim`). + ylim : tuple or list, optional + Set limits of y axis (see `matplotlib.axes.Axes.set_ylim`). Returns ------- @@ -75,7 +79,7 @@ def root_locus_map(sysdata, gains=None): nump, denp = _systopoly1d(sys[0, 0]) if gains is None: - kvect, root_array, _, _ = _default_gains(nump, denp, None, None) + kvect, root_array, _, _ = _default_gains(nump, denp, xlim, ylim) else: kvect = np.atleast_1d(gains) root_array = _RLFindRoots(nump, denp, kvect) @@ -205,6 +209,11 @@ def root_locus_plot( # Plot the root loci cplt = responses.plot(grid=grid, **kwargs) + # Add a reaction to axis scale changes, if given LTI systems, and + # there is no set of pre-defined gains + if gains is None: + add_loci_recalculate(sysdata, cplt, cplt.axes[0,0]) + # Legacy processing: return locations of poles and zeros as a tuple if plot is True: return responses.loci, responses.gains @@ -212,6 +221,40 @@ def root_locus_plot( return ControlPlot(cplt.lines, cplt.axes, cplt.figure) +def add_loci_recalculate(sysdata, cplt, axis): + """Add a callback to re-calculate the loci data fitting a zoom action. + + Parameters + ---------- + sysdata: LTI object or list + Linear input/output systems (SISO only, for now). + cplt: ControlPlot + Collection of plot handles. + axis: matplotlib.axes.Axis + Axis on which callbacks are installed. + """ + + # if LTI, treat everything as a list of lti + if isinstance(sysdata, LTI): + sysdata = [sysdata] + + # check that we can actually recalculate the loci + if isinstance(sysdata, list) and all( + [isinstance(sys, LTI) for sys in sysdata]): + + # callback function for axis change (zoom, pan) events + # captures the sysdata object and cplt + def _zoom_adapter(_ax): + newresp = root_locus_map(sysdata, None, + _ax.get_xlim(), + _ax.get_ylim()) + newresp.replot(cplt) + + # connect the callback to axis changes + axis.callbacks.connect('xlim_changed', _zoom_adapter) + axis.callbacks.connect('ylim_changed', _zoom_adapter) + + def _default_gains(num, den, xlim, ylim): """Unsupervised gains calculation for root locus plot. @@ -288,7 +331,7 @@ def _default_gains(num, den, xlim, ylim): # Root locus is on imaginary axis (rare), use just y distance tolerance = y_tolerance elif y_tolerance == 0: - # Root locus is on imaginary axis (common), use just x distance + # Root locus is on real axis (common), use just x distance tolerance = x_tolerance else: tolerance = np.min([x_tolerance, y_tolerance]) diff --git a/control/sisotool.py b/control/sisotool.py index 78be86b16..2d4506f52 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -22,6 +22,7 @@ from .statesp import ss, summing_junction from .timeresp import step_response from .xferfcn import tf +from .rlocus import add_loci_recalculate _sisotool_defaults = { 'sisotool.initial_gain': 1 @@ -105,7 +106,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, fig = plt.gcf() if fig.canvas.manager.get_window_title() != 'Sisotool': plt.close(fig) - fig,axes = plt.subplots(2, 2) + fig, axes = plt.subplots(2, 2) fig.canvas.manager.set_window_title('Sisotool') else: axes = np.array(fig.get_axes()).reshape(2, 2) @@ -137,8 +138,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, # sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus, # ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, # ax=fig.axes[1]) - ax_rlocus = fig.axes[1] - root_locus_map(sys[0, 0]).plot( + ax_rlocus = axes[0,1] # fig.axes[1] + cplt = root_locus_map(sys[0, 0]).plot( xlim=xlim_rlocus, ylim=ylim_rlocus, initial_gain=initial_gain, ax=ax_rlocus) if rlocus_grid is False: @@ -146,6 +147,9 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, from .grid import nogrid nogrid(sys.dt, ax=ax_rlocus) + # install a zoom callback on the root-locus axis + add_loci_recalculate(sys, cplt, ax_rlocus) + # Reset the button release callback so that we can update all plots fig.canvas.mpl_connect( 'button_release_event', partial( @@ -155,9 +159,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, def _click_dispatcher(event, sys, ax, bode_plot_params, tvect): # Zoom handled by specialized callback in rlocus, only handle gain plot - if event.inaxes == ax.axes and \ - plt.get_current_fig_manager().toolbar.mode not in \ - {'zoom rect', 'pan/zoom'}: + if event.inaxes == ax.axes: + fig = ax.figure # if a point is clicked on the rootlocus plot visually emphasize it pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy