@@ -1101,13 +1101,14 @@ def gen_zero_centered_series(val_min, val_max, period):
11011101_nyquist_defaults = {
11021102 'nyquist.primary_style' : ['-' , '-.' ], # style for primary curve
11031103 'nyquist.mirror_style' : ['--' , ':' ], # style for mirror curve
1104- 'nyquist.arrows' : 2 , # number of arrows around curve
1104+ 'nyquist.arrows' : 3 , # number of arrows around curve
11051105 'nyquist.arrow_size' : 8 , # pixel size for arrows
11061106 'nyquist.encirclement_threshold' : 0.05 , # warning threshold
11071107 'nyquist.indent_radius' : 1e-4 , # indentation radius
11081108 'nyquist.indent_direction' : 'right' , # indentation direction
1109- 'nyquist.indent_points' : 50 , # number of points to insert
1110- 'nyquist.max_curve_magnitude' : 20 , # clip large values
1109+ 'nyquist.indent_points' : 200 , # number of points to insert
1110+ 'nyquist.max_curve_magnitude' : 15 , # rescale large values
1111+ 'nyquist.blend_fraction' : 0.15 , # when to start scaling
11111112 'nyquist.max_curve_offset' : 0.02 , # offset of primary/mirror
11121113 'nyquist.start_marker' : 'o' , # marker at start of curve
11131114 'nyquist.start_marker_size' : 4 , # size of the marker
@@ -1639,6 +1640,10 @@ def nyquist_plot(
16391640 The matplotlib axes to draw the figure on. If not specified and
16401641 the current figure has a single axes, that axes is used.
16411642 Otherwise, a new figure is created.
1643+ blend_fraction : float, optional
1644+ For portions of the Nyquist curve that are scaled to have a maximum
1645+ magnitude of `max_curve_magnitude`, begin a smooth rescaling at
1646+ this fraction of `max_curve_magnitude`. Default value is 0.15.
16421647 encirclement_threshold : float, optional
16431648 Define the threshold for generating a warning if the number of net
16441649 encirclements is a non-integer value. Default value is 0.05 and can
@@ -1655,7 +1660,7 @@ def nyquist_plot(
16551660 portions of the contour are plotted using a different line style.
16561661 label : str or array_like of str, optional
16571662 If present, replace automatically generated label(s) with the given
1658- label(s). If sysdata is a list, strings should be specified for each
1663+ label(s). If `data` is a list, strings should be specified for each
16591664 system.
16601665 label_freq : int, optional
16611666 Label every nth frequency on the plot. If not specified, no labels
@@ -1686,8 +1691,8 @@ def nyquist_plot(
16861691 elements is equivalent to providing `omega_limits`.
16871692 omega_num : int, optional
16881693 Number of samples to use for the frequency range. Defaults to
1689- `config.defaults['freqplot.number_of_samples']`. Ignored if data is
1690- not a list of systems.
1694+ `config.defaults['freqplot.number_of_samples']`. Ignored if ` data`
1695+ is not a system or list of systems.
16911696 plot : bool, optional
16921697 (legacy) If given, `nyquist_plot` returns the legacy return values
16931698 of (counts, contours). If False, return the values with no plot.
@@ -1752,8 +1757,8 @@ def nyquist_plot(
17521757 to avoid poles, resulting in a scaling of the Nyquist plot, the line
17531758 styles are according to the settings of the `primary_style` and
17541759 `mirror_style` keywords. By default the scaled portions of the primary
1755- curve use a dotted line style and the scaled portion of the mirror
1756- image use a dashdot line style.
1760+ curve use a dashdot line style and the scaled portions of the mirror
1761+ image use a dotted line style.
17571762
17581763 Examples
17591764 --------
@@ -1785,6 +1790,8 @@ def nyquist_plot(
17851790 ax_user = ax
17861791 max_curve_magnitude = config ._get_param (
17871792 'nyquist' , 'max_curve_magnitude' , kwargs , _nyquist_defaults , pop = True )
1793+ blend_fraction = config ._get_param (
1794+ 'nyquist' , 'blend_fraction' , kwargs , _nyquist_defaults , pop = True )
17881795 max_curve_offset = config ._get_param (
17891796 'nyquist' , 'max_curve_offset' , kwargs , _nyquist_defaults , pop = True )
17901797 rcParams = config ._get_param ('ctrlplot' , 'rcParams' , kwargs , pop = True )
@@ -1879,10 +1886,16 @@ def _parse_linestyle(style_name, allow_false=False):
18791886 legend_loc , _ , show_legend = _process_legend_keywords (
18801887 kwargs , None , 'upper right' )
18811888
1889+ # Figure out where the blended curve should start
1890+ if blend_fraction < 0 or blend_fraction > 1 :
1891+ raise ValueError ("blend_fraction must be between 0 and 1" )
1892+ blend_curve_start = (1 - blend_fraction ) * max_curve_magnitude
1893+
18821894 # Create a list of lines for the output
1883- out = np .empty (len (nyquist_responses ), dtype = object )
1884- for i in range (out .shape [0 ]):
1885- out [i ] = [] # unique list in each element
1895+ out = np .empty ((len (nyquist_responses ), 4 ), dtype = object )
1896+ for i in range (len (nyquist_responses )):
1897+ for j in range (4 ):
1898+ out [i , j ] = [] # unique list in each element
18861899
18871900 for idx , response in enumerate (nyquist_responses ):
18881901 resp = response .response
@@ -1893,20 +1906,31 @@ def _parse_linestyle(style_name, allow_false=False):
18931906
18941907 # Find the different portions of the curve (with scaled pts marked)
18951908 reg_mask = np .logical_or (
1896- np .abs (resp ) > max_curve_magnitude ,
1897- splane_contour .real != 0 )
1898- # reg_mask = np.logical_or(
1899- # np.abs(resp.real) > max_curve_magnitude,
1900- # np.abs(resp.imag) > max_curve_magnitude)
1909+ np .abs (resp ) > blend_curve_start ,
1910+ np .logical_not (np .isclose (splane_contour .real , 0 )))
19011911
19021912 scale_mask = ~ reg_mask \
19031913 & np .concatenate ((~ reg_mask [1 :], ~ reg_mask [- 1 :])) \
19041914 & np .concatenate ((~ reg_mask [0 :1 ], ~ reg_mask [:- 1 ]))
19051915
19061916 # Rescale the points with large magnitude
1907- rescale = np .logical_and (
1908- reg_mask , abs (resp ) > max_curve_magnitude )
1909- resp [rescale ] *= max_curve_magnitude / abs (resp [rescale ])
1917+ rescale_idx = (np .abs (resp ) > blend_curve_start )
1918+
1919+ if np .any (rescale_idx ): # Only process if rescaling is needed
1920+ subset = resp [rescale_idx ]
1921+ abs_subset = np .abs (subset )
1922+ unit_vectors = subset / abs_subset # Preserve phase/direction
1923+
1924+ if blend_curve_start == max_curve_magnitude :
1925+ # Clip at max_curve_magnitude
1926+ resp [rescale_idx ] = max_curve_magnitude * unit_vectors
1927+ else :
1928+ # Logistic scaling
1929+ newmag = blend_curve_start + \
1930+ (max_curve_magnitude - blend_curve_start ) * \
1931+ (abs_subset - blend_curve_start ) / \
1932+ (abs_subset + max_curve_magnitude - 2 * blend_curve_start )
1933+ resp [rescale_idx ] = newmag * unit_vectors
19101934
19111935 # Get the label to use for the line
19121936 label = response .sysname if line_labels is None else line_labels [idx ]
@@ -1917,7 +1941,7 @@ def _parse_linestyle(style_name, allow_false=False):
19171941 p = ax .plot (
19181942 x_reg , y_reg , primary_style [0 ], color = color , label = label , ** kwargs )
19191943 c = p [0 ].get_color ()
1920- out [idx ] += p
1944+ out [idx , 0 ] += p
19211945
19221946 # Figure out how much to offset the curve: the offset goes from
19231947 # zero at the start of the scaled section to max_curve_offset as
@@ -1929,12 +1953,12 @@ def _parse_linestyle(style_name, allow_false=False):
19291953 x_scl = np .ma .masked_where (scale_mask , resp .real )
19301954 y_scl = np .ma .masked_where (scale_mask , resp .imag )
19311955 if x_scl .count () >= 1 and y_scl .count () >= 1 :
1932- out [idx ] += ax .plot (
1956+ out [idx , 1 ] += ax .plot (
19331957 x_scl * (1 + curve_offset ),
19341958 y_scl * (1 + curve_offset ),
19351959 primary_style [1 ], color = c , ** kwargs )
19361960 else :
1937- out [idx ] += [None ]
1961+ out [idx , 1 ] += [None ]
19381962
19391963 # Plot the primary curve (invisible) for setting arrows
19401964 x , y = resp .real .copy (), resp .imag .copy ()
@@ -1949,15 +1973,15 @@ def _parse_linestyle(style_name, allow_false=False):
19491973 # Plot the mirror image
19501974 if mirror_style is not False :
19511975 # Plot the regular and scaled segments
1952- out [idx ] += ax .plot (
1976+ out [idx , 2 ] += ax .plot (
19531977 x_reg , - y_reg , mirror_style [0 ], color = c , ** kwargs )
19541978 if x_scl .count () >= 1 and y_scl .count () >= 1 :
1955- out [idx ] += ax .plot (
1979+ out [idx , 3 ] += ax .plot (
19561980 x_scl * (1 - curve_offset ),
19571981 - y_scl * (1 - curve_offset ),
19581982 mirror_style [1 ], color = c , ** kwargs )
19591983 else :
1960- out [idx ] += [None ]
1984+ out [idx , 3 ] += [None ]
19611985
19621986 # Add the arrows (on top of an invisible contour)
19631987 x , y = resp .real .copy (), resp .imag .copy ()
@@ -1967,12 +1991,15 @@ def _parse_linestyle(style_name, allow_false=False):
19671991 _add_arrows_to_line2D (
19681992 ax , p [0 ], arrow_pos , arrowstyle = arrow_style , dir = - 1 )
19691993 else :
1970- out [idx ] += [None , None ]
1994+ out [idx , 2 ] += [None ]
1995+ out [idx , 3 ] += [None ]
19711996
19721997 # Mark the start of the curve
19731998 if start_marker :
1974- ax .plot (resp [0 ].real , resp [0 ].imag , start_marker ,
1975- color = c , markersize = start_marker_size )
1999+ segment = 0 if 0 in rescale_idx else 1 # regular vs scaled
2000+ out [idx , segment ] += ax .plot (
2001+ resp [0 ].real , resp [0 ].imag , start_marker ,
2002+ color = c , markersize = start_marker_size )
19762003
19772004 # Mark the -1 point
19782005 ax .plot ([- 1 ], [0 ], 'r+' )
0 commit comments