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)
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