Skip to content

Commit 4ef4906

Browse files
committed
fix rlocus timeout due to inefficient _default_wn calculation
1 parent 343d5d6 commit 4ef4906

File tree

2 files changed

+47
-10
lines changed

2 files changed

+47
-10
lines changed

control/rlocus.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -646,8 +646,9 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
646646
else:
647647
ax = fig.axes[1]
648648

649-
# Get locator function for x-axis tick marks
649+
# Get locator function for x-axis, y-axis tick marks
650650
xlocator = ax.get_xaxis().get_major_locator()
651+
ylocator = ax.get_yaxis().get_major_locator()
651652

652653
# Decide on the location for the labels (?)
653654
ylim = ax.get_ylim()
@@ -690,7 +691,7 @@ def _sgrid_func(fig=None, zeta=None, wn=None):
690691
# omega-constant lines
691692
angles = np.linspace(-90, 90, 20) * np.pi/180
692693
if wn is None:
693-
wn = _default_wn(xlocator(), ylim)
694+
wn = _default_wn(xlocator(), ylocator())
694695

695696
for om in wn:
696697
if om < 0:
@@ -746,7 +747,7 @@ def _default_zetas(xlim, ylim):
746747
return zeta.tolist()
747748

748749

749-
def _default_wn(xloc, ylim):
750+
def _default_wn(xloc, yloc, max_lines=7):
750751
"""Return default wn for root locus plot
751752
752753
This function computes a list of natural frequencies based on the grid
@@ -758,23 +759,30 @@ def _default_wn(xloc, ylim):
758759
List of x-axis tick values
759760
ylim : array_like
760761
List of y-axis limits [min, max]
762+
max_lines : int, optional
763+
Maximum number of frequencies to generate (default = 7)
761764
762765
Returns
763766
-------
764767
wn : list
765768
List of default natural frequencies for the plot
766769
767770
"""
771+
sep = xloc[1]-xloc[0] # separation between x-ticks
772+
773+
# Decide whether to use the x or y axis for determining wn
774+
if yloc[-1] / sep > max_lines*10:
775+
# y-axis scale >> x-axis scale
776+
wn = yloc # one frequency per y-axis tick mark
777+
else:
778+
wn = xloc # one frequency per x-axis tick mark
768779

769-
wn = xloc # one frequency per x-axis tick mark
770-
sep = xloc[1]-xloc[0] # separation between ticks
771-
772-
# Insert additional frequencies to span the y-axis
773-
while np.abs(wn[0]) < ylim[1]:
774-
wn = np.insert(wn, 0, wn[0]-sep)
780+
# Insert additional frequencies to span the y-axis
781+
while np.abs(wn[0]) < yloc[-1]:
782+
wn = np.insert(wn, 0, wn[0]-sep)
775783

776784
# If there are too many values, cut them in half
777-
while len(wn) > 7:
785+
while len(wn) > max_lines:
778786
wn = wn[0:-1:2]
779787

780788
return wn

control/tests/rlocus_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from numpy.testing import assert_array_almost_equal
99
import pytest
1010

11+
import control as ct
1112
from control.rlocus import root_locus, _RLClickDispatcher
1213
from control.xferfcn import TransferFunction
1314
from control.statesp import StateSpace
@@ -74,3 +75,31 @@ def test_root_locus_zoom(self):
7475

7576
assert_array_almost_equal(zoom_x, zoom_x_valid)
7677
assert_array_almost_equal(zoom_y, zoom_y_valid)
78+
79+
def test_rlocus_default_wn(self):
80+
"""Check that default wn calculation works properly"""
81+
#
82+
# System that triggers use of y-axis as basis for wn (for coverage)
83+
#
84+
# This system generates a root locus plot that used to cause the
85+
# creation (and subsequent deletion) of a large number of natural
86+
# frequency contours within the `_default_wn` function in `rlocus.py`.
87+
# This unit test makes sure that is fixed by generating a test case
88+
# that will take a long time to do the calculation (minutes).
89+
#
90+
import scipy as sp
91+
import signal
92+
93+
# Define a system that exhibits this behavior
94+
sys = ct.tf(*sp.signal.zpk2tf(
95+
[-1e-2, 1-1e7j, 1+1e7j], [0, -1e7j, 1e7j], 1))
96+
97+
# Set up a timer to catch execution time
98+
def signal_handler(signum, frame):
99+
raise Exception("rlocus took too long to complete")
100+
signal.signal(signal.SIGALRM, signal_handler)
101+
102+
# Run the command and reset the alarm
103+
signal.alarm(2) # 2 second timeout
104+
ct.root_locus(sys)
105+
signal.alarm(0) # reset the alarm

0 commit comments

Comments
 (0)