8000 Merge branch 'python-control:main' into jdelange/disk-margins · python-control/python-control@7186406 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7186406

Browse files
Merge branch 'python-control:main' into jdelange/disk-margins
2 parents b85147e + 34c6d59 commit 7186406

File tree

3 files changed

+95
-9
lines changed

3 files changed

+95
-9
lines changed

control/pzmap.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ def plot(self, *args, **kwargs):
124124
"""
125125
return pole_zero_plot(self, *args, **kwargs)
126126

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+
127138

128139
# Pole/zero map
129140
def pole_zero_map(sysdata):
@@ -513,6 +524,35 @@ def _click_dispatcher(event):
513524
return ControlPlot(out, ax, fig, legend=legend)
514525

515526

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+
516556
# Utility function to find gain corresponding to a click event
517557
def _find_root_locus_gain(event, sys, ax):
518558
# Get the current axis limits to set various thresholds

control/rlocus.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
# Root locus map
36-
def root_locus_map(sysdata, gains=None):
36+
def root_locus_map(sysdata, gains=None, xlim=None, ylim=None):
3737
"""Compute the root locus map for an LTI system.
3838
3939
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):
4646
gains : array_like, optional
4747
Gains to use in computing plot of closed-loop poles. If not given,
4848
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`).
4953
5054
Returns
5155
-------
@@ -75,7 +79,7 @@ def root_locus_map(sysdata, gains=None):
7579
nump, denp = _systopoly1d(sys[0, 0])
7680

7781
if gains is None:
78-
kvect, root_array, _, _ = _default_gains(nump, denp, None, None)
82+
kvect, root_array, _, _ = _default_gains(nump, denp, xlim, ylim)
7983
else:
8084
kvect = np.atleast_1d(gains)
8185
root_array = _RLFindRoots(nump, denp, kvect)
@@ -205,13 +209,52 @@ def root_locus_plot(
205209
# Plot the root loci
206210
cplt = responses.plot(grid=grid, **kwargs)
207211

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+
208217
# Legacy processing: return locations of poles and zeros as a tuple
209218
if plot is True:
210219
return responses.loci, responses.gains
211220

212221
return ControlPlot(cplt.lines, cplt.axes, cplt.figure)
213222

214223

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+
215258
def _default_gains(num, den, xlim, ylim):
216259
"""Unsupervised gains calculation for root locus plot.
217260
@@ -288,7 +331,7 @@ def _default_gains(num, den, xlim, ylim):
288331
# Root locus is on imaginary axis (rare), use just y distance
289332
tolerance = y_tolerance
290333
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
292335
tolerance = x_tolerance
293336
else:
294337
tolerance = np.min([x_tolerance, y_tolerance])

control/sisotool.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .statesp import ss, summing_junction
2323
from .timeresp import step_response
2424
from .xferfcn import tf
25+
from .rlocus import add_loci_recalculate
2526

2627
_sisotool_defaults = {
2728
'sisotool.initial_gain': 1
@@ -105,7 +106,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None,
105106
fig = plt.gcf()
106107
if fig.canvas.manager.get_window_title() != 'Sisotool':
107108
plt.close(fig)
108-
fig,axes = plt.subplots(2, 2)
109+
fig, axes = plt.subplots(2, 2)
109110
fig.canvas.manager.set_window_title('Sisotool')
110111
else:
111112
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,
137138
# sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus,
138139
# ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid,
139140
# 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(
142143
xlim=xlim_rlocus, ylim=ylim_rlocus,
143144
initial_gain=initial_gain, ax=ax_rlocus)
144145
if rlocus_grid is False:
145146
# Need to generate grid manually, since root_locus_plot() won't
146147
from .grid import nogrid
147148
nogrid(sys.dt, ax=ax_rlocus)
148149

150+
# install a zoom callback on the root-locus axis
151+
add_loci_recalculate(sys, cplt, ax_rlocus)
152+
149153
# Reset the button release callback so that we can update all plots
150154
fig.canvas.mpl_connect(
151155
'button_release_event', partial(
@@ -155,9 +159,8 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None,
155159

156160
def _click_dispatcher(event, sys, ax, bode_plot_params, tvect):
157161
# 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+
161164
fig = ax.figure
162165

163166
# if a point is clicked on the rootlocus plot visually emphasize it

0 commit comments

Comments
 (0)
0