Skip to content

Commit ca6f3da

Browse files
committed
Update Nyquist rescaling + other improvements
1 parent 632391c commit ca6f3da

File tree

2 files changed

+87
-14
lines changed

2 files changed

+87
-14
lines changed

control/freqplot.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,13 +1100,14 @@ def gen_zero_centered_series(val_min, val_max, period):
11001100
_nyquist_defaults = {
11011101
'nyquist.primary_style': ['-', '-.'], # style for primary curve
11021102
'nyquist.mirror_style': ['--', ':'], # style for mirror curve
1103-
'nyquist.arrows': 2, # number of arrows around curve
1103+
'nyquist.arrows': 3, # number of arrows around curve
11041104
'nyquist.arrow_size': 8, # pixel size for arrows
11051105
'nyquist.encirclement_threshold': 0.05, # warning threshold
11061106
'nyquist.indent_radius': 1e-4, # indentation radius
11071107
'nyquist.indent_direction': 'right', # indentation direction
1108-
'nyquist.indent_points': 50, # number of points to insert
1109-
'nyquist.max_curve_magnitude': 20, # clip large values
1108+
'nyquist.indent_points': 200, # number of points to insert
1109+
'nyquist.max_curve_magnitude': 15, # rescale large values
1110+
'nyquist.blend_fraction': 0.15, # when to start scaling
11101111
'nyquist.max_curve_offset': 0.02, # offset of primary/mirror
11111112
'nyquist.start_marker': 'o', # marker at start of curve
11121113
'nyquist.start_marker_size': 4, # size of the marker
@@ -1638,6 +1639,10 @@ def nyquist_plot(
16381639
The matplotlib axes to draw the figure on. If not specified and
16391640
the current figure has a single axes, that axes is used.
16401641
Otherwise, a new figure is created.
1642+
blend_fraction : float, optional
1643+
For portions of the Nyquist curve that are scaled to have a maximum
1644+
magnitude of `max_curve_magnitude`, begin a smooth rescaling at
1645+
this fraction of `max_curve_magnitude`. Default value is 0.15.
16411646
encirclement_threshold : float, optional
16421647
Define the threshold for generating a warning if the number of net
16431648
encirclements is a non-integer value. Default value is 0.05 and can
@@ -1784,6 +1789,8 @@ def nyquist_plot(
17841789
ax_user = ax
17851790
max_curve_magnitude = config._get_param(
17861791
'nyquist', 'max_curve_magnitude', kwargs, _nyquist_defaults, pop=True)
1792+
blend_fraction = config._get_param(
1793+
'nyquist', 'blend_fraction', kwargs, _nyquist_defaults, pop=True)
17871794
max_curve_offset = config._get_param(
17881795
'nyquist', 'max_curve_offset', kwargs, _nyquist_defaults, pop=True)
17891796
rcParams = config._get_param('ctrlplot', 'rcParams', kwargs, pop=True)
@@ -1891,21 +1898,36 @@ def _parse_linestyle(style_name, allow_false=False):
18911898
splane_contour = np.log(response.contour) / response.dt
18921899

18931900
# Find the different portions of the curve (with scaled pts marked)
1901+
if blend_fraction < 0 or blend_fraction > 1:
1902+
raise ValueError("blend_fraction must be between 0 and 1")
1903+
blend_curve_start = (1 - blend_fraction) * max_curve_magnitude
18941904
reg_mask = np.logical_or(
1895-
np.abs(resp) > max_curve_magnitude,
1896-
splane_contour.real != 0)
1897-
# reg_mask = np.logical_or(
1898-
# np.abs(resp.real) > max_curve_magnitude,
1899-
# np.abs(resp.imag) > max_curve_magnitude)
1905+
np.abs(resp) > blend_curve_start,
1906+
np.logical_not(np.isclose(splane_contour.real, 0)))
19001907

19011908
scale_mask = ~reg_mask \
19021909
& np.concatenate((~reg_mask[1:], ~reg_mask[-1:])) \
19031910
& np.concatenate((~reg_mask[0:1], ~reg_mask[:-1]))
19041911

19051912
# Rescale the points with large magnitude
1906-
rescale = np.logical_and(
1907-
reg_mask, abs(resp) > max_curve_magnitude)
1908-
resp[rescale] *= max_curve_magnitude / abs(resp[rescale])
1913+
rescale_idx = (np.abs(resp) > blend_curve_start)
1914+
1915+
if np.any(rescale_idx): # Only process if rescaling is needed
1916+
subset = resp[rescale_idx]
1917+
abs_subset = np.abs(subset)
1918+
unit_vectors = subset / abs_subset # Preserve phase/direction
1919+
1920+
if blend_curve_start is None or \
1921+
blend_curve_start == max_curve_magnitude:
1922+
# Clip at max_curve_magnitude
1923+
resp[rescale_idx] = max_curve_magnitude * unit_vectors
1924+
else:
1925+
# Logistic scaling
1926+
newmag = blend_curve_start + \
1927+
(max_curve_magnitude - blend_curve_start) * \
1928+
(abs_subset - blend_curve_start) / \
1929+
(abs_subset + max_curve_magnitude - 2 * blend_curve_start)
1930+
resp[rescale_idx] = newmag * unit_vectors
19091931

19101932
# Get the label to use for the line
19111933
label = response.sysname if line_labels is None else line_labels[idx]

control/tests/nyquist_test.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_nyquist_indent_default(indentsys):
291291

292292
def test_nyquist_indent_dont(indentsys):
293293
# first value of default omega vector was 0.1, replaced by 0. for contour
294-
# indent_radius is larger than 0.1 -> no extra quater circle around origin
294+
# indent_radius is larger than 0.1 -> no extra quarter circle around origin
295295
with pytest.warns() as record:
296296
count, contour = ct.nyquist_response(
297297
indentsys, omega=[0, 0.2, 0.3, 0.4], indent_radius=.1007,
@@ -428,6 +428,7 @@ def test_linestyle_checks():
428428
ct.nyquist_plot(sys, primary_style=':', mirror_style='-.')
429429

430430
@pytest.mark.usefixtures("editsdefaults")
431+
@pytest.mark.xfail(reason="updated code avoids warning")
431432
def test_nyquist_legacy():
432433
ct.use_legacy_defaults('0.9.1')
433434

@@ -526,6 +527,34 @@ def test_no_indent_pole():
526527
sys, warn_encirclements=False, indent_direction='none')
527528

528529

530+
def test_nyquist_rescale():
531+
sys = 2 * ct.tf([1], [1, 1]) * ct.tf([1], [1, 0])**2
532+
sys.name = 'How example'
533+
534+
# Default case
535+
cplt = ct.nyquist_plot(sys, indent_direction='left', label='default [0.15]')
536+
537+
# Sharper corner
538+
cplt = ct.nyquist_plot(
539+
sys*4, indent_direction='left',
540+
max_curve_magnitude=17, blend_fraction=0.05, label='fraction=0.05')
541+
542+
# More gradual corner
543+
cplt = ct.nyquist_plot(
544+
sys*0.25, indent_direction='left',
545+
max_curve_magnitude=13, blend_fraction=0.25, label='fraction=0.25')
546+
547+
# No corner
548+
cplt = ct.nyquist_plot(
549+
sys*12, indent_direction='left',
550+
max_curve_magnitude=19, blend_fraction=0, label='fraction=0')
551+
552+
# Bad value
553+
with pytest.raises(ValueError, match="blend_fraction must be between"):
554+
cplt = ct.nyquist_plot(
555+
sys, indent_direction='left', blend_fraction=1.2)
556+
557+
529558
if __name__ == "__main__":
530559
#
531560
# Interactive mode: generate plots for manual viewing
@@ -566,8 +595,8 @@ def test_no_indent_pole():
566595
sys = 3 * (s+6)**2 / (s * (s**2 + 1e-4 * s + 1))
567596
plt.figure()
568597
ct.nyquist_plot(sys)
569-
ct.nyquist_plot(sys, max_curve_magnitude=15)
570-
ct.nyquist_plot(sys, indent_radius=1e-6, max_curve_magnitude=25)
598+
ct.nyquist_plot(sys, max_curve_magnitude=10)
599+
ct.nyquist_plot(sys, indent_radius=1e-6, max_curve_magnitude=20)
571600

572601
print("Unusual Nyquist plot")
573602
sys = ct.tf([1], [1, 3, 2]) * ct.tf([1], [1, 0, 1])
@@ -595,3 +624,25 @@ def test_no_indent_pole():
595624
plt.figure()
596625
cplt = ct.nyquist_plot([sys, sys1, sys2])
597626
cplt.set_plot_title("Mixed FRD, tf data")
627+
628+
plt.figure()
629+
print("Jon How example")
630+
test_nyquist_rescale()
631+
632+
#
633+
# Save the figures in a PDF file for later comparisons
634+
#
635+
import subprocess
636+
from matplotlib.backends.backend_pdf import PdfPages
637+
from datetime import date
638+
639+
# Create the file to store figures
640+
git_info = subprocess.check_output(['git', 'describe'], text=True).strip()
641+
pdf = PdfPages(
642+
f'nyquist_gallery-{git_info}-{date.today().isoformat()}.pdf')
643+
644+
# Go through each figure and save it
645+
for fignum in plt.get_fignums():
646+
pdf.savefig(plt.figure(fignum))
647+
648+
pdf.close()

0 commit comments

Comments
 (0)