@@ -519,11 +519,15 @@ def gen_zero_centered_series(val_min, val_max, period):
519519
520520# Default values for module parameter variables
521521_nyquist_defaults = {
522- 'nyquist.mirror_style' : '--' ,
522+ 'nyquist.primary_style' : ['-' , ':' ], # style for primary curve
523+ 'nyquist.mirror_style' : ['--' , '-.' ], # style for mirror curve
523524 'nyquist.arrows' : 2 ,
524525 'nyquist.arrow_size' : 8 ,
525- 'nyquist.indent_radius' : 1e-1 ,
526- 'nyquist.indent_direction' : 'right' ,
526+ 'nyquist.indent_radius' : 1e-6 , # indentation radius
527+ 'nyquist.indent_direction' : 'right' , # indentation direction
528+ 'nyquist.indent_points' : 50 , # number of points to insert
529+ 'nyquist.max_curve_magnitude' : 20 ,
530+ 'nyquist.max_curve_offset' : 0.02 , # percent offset of curves
527531}
528532
529533
@@ -563,19 +567,32 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
563567 color : string
564568 Used to specify the color of the line and arrowhead.
565569
566- mirror_style : string or False
567- Linestyle for mirror image of the Nyquist curve. If `False` then
568- omit completely. Default linestyle ('--') is determined by
569- config.defaults['nyquist.mirror_style'].
570-
571- return_contour : bool
570+ return_contour : bool, optional
572571 If 'True', return the contour used to evaluate the Nyquist plot.
573572
574- label_freq : int
573+ *args : :func:`matplotlib.pyplot.plot` positional properties, optional
574+ Additional arguments for `matplotlib` plots (color, linestyle, etc)
575+
576+ **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
577+ Additional keywords (passed to `matplotlib`)
578+
579+ Returns
580+ -------
581+ count : int (or list of int if len(syslist) > 1)
582+ Number of encirclements of the point -1 by the Nyquist curve. If
583+ multiple systems are given, an array of counts is returned.
584+
585+ contour : ndarray (or list of ndarray if len(syslist) > 1)), optional
586+ The contour used to create the primary Nyquist curve segment. To
587+ obtain the Nyquist curve values, evaluate system(s) along contour.
588+
589+ Additional Parameters
590+ ---------------------
591+ label_freq : int, optiona
575592 Label every nth frequency on the plot. If not specified, no labels
576593 are generated.
577594
578- arrows : int or 1D/2D array of floats
595+ arrows : int or 1D/2D array of floats, optional
579596 Specify the number of arrows to plot on the Nyquist curve. If an
580597 integer is passed. that number of equally spaced arrows will be
581598 plotted on each of the primary segment and the mirror image. If a 1D
@@ -585,39 +602,51 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
585602 locations for the primary curve and the second row will be used for
586603 the mirror image.
587604
588- arrow_size : float
605+ arrow_size : float, optional
589606 Arrowhead width and length (in display coordinates). Default value is
590607 8 and can be set using config.defaults['nyquist.arrow_size'].
591608
592- arrow_style : matplotlib.patches.ArrowStyle
609+ arrow_style : matplotlib.patches.ArrowStyle, optional
593610 Define style used for Nyquist curve arrows (overrides `arrow_size`).
594611
595- indent_radius : float
596- Amount to indent the Nyquist contour around poles that are at or near
597- the imaginary axis.
598-
599- indent_direction : str
612+ indent_direction : str, optional
600613 For poles on the imaginary axis, set the direction of indentation to
601614 be 'right' (default), 'left', or 'none'.
602615
603- warn_nyquist : bool, optional
604- If set to 'False', turn off warnings about frequencies above Nyquist.
605-
606- *args : :func:`matplotlib.pyplot.plot` positional properties, optional
607- Additional arguments for `matplotlib` plots (color, linestyle, etc)
616+ indent_points : int, optional
617+ Number of points to insert in the Nyquist contour around poles that
618+ are at or near the imaginary axis.
608619
609- **kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
610- Additional keywords (passed to `matplotlib`)
620+ indent_radius : float, optional
621+ Amount to indent the Nyquist contour around poles that are at or near
622+ the imaginary axis.
611623
612- Returns
613- -------
614- count : int (or list of int if len(syslist) > 1)
615- Number of encirclements of the point -1 by the Nyquist curve. If
616- multiple systems are given, an array of counts is returned.
624+ max_curve_magnitude : float, optional
625+ Restrict the maximum magnitude of the Nyquist plot to this value.
626+ Portions of the Nyquist plot whose magnitude is restricted are
627+ plotted using a different line style.
628+
629+ max_curve_offset : float, optional
630+ When plotting scaled portion of the Nyquist plot, increase/decrease
631+ the magnitude by this fraction of the max_curve_magnitude to allow
632+ any overlaps between the primary and mirror curves to be avoided.
633+
634+ mirror_style : [str, str] or False
635+ Linestyles for mirror image of the Nyquist curve. The first element
636+ is used for unscaled portions of the Nyquist curve, the second element
637+ is used for portions that are scaled (using max_curve_magnitude). If
638+ `False` then omit completely. Default linestyle (['--', '-.']) is
639+ determined by config.defaults['nyquist.mirror_style'].
640+
641+ primary_style : [str, str]
642+ Linestyles for primary image of the Nyquist curve. The first element
643+ is used for unscaled portions of the Nyquist curve, the second
644+ element is used for scaled portions that are scaled (using
645+ max_curve_magnitude). Default linestyle (['-', ':']) is determined by
646+ config.defaults['nyquist.mirror_style'].
617647
618- contour : ndarray (or list of ndarray if len(syslist) > 1)), optional
619- The contour used to create the primary Nyquist curve segment. To
620- obtain the Nyquist curve values, evaluate system(s) along contour.
648+ warn_nyquist : bool, optional
649+ If set to 'False', turn off warnings about frequencies above Nyquist.
621650
622651 Notes
623652 -----
@@ -668,8 +697,6 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
668697
669698 # Get values for params (and pop from list to allow keyword use in plot)
670699 omega_num = config ._get_param ('freqplot' , 'number_of_samples' , omega_num )
671- mirror_style = config ._get_param (
672- 'nyquist' , 'mirror_style' , kwargs , _nyquist_defaults , pop = True )
673700 arrows = config ._get_param (
674701 'nyquist' , 'arrows' , kwargs , _nyquist_defaults , pop = True )
675702 arrow_size = config ._get_param (
@@ -679,6 +706,28 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
679706 'nyquist' , 'indent_radius' , kwargs , _nyquist_defaults , pop = True )
680707 indent_direction = config ._get_param (
681708 'nyquist' , 'indent_direction' , kwargs , _nyquist_defaults , pop = True )
709+ indent_points = config ._get_param (
710+ 'nyquist' , 'indent_points' , kwargs , _nyquist_defaults , pop = True )
711+ max_curve_magnitude = config ._get_param (
712+ 'nyquist' , 'max_curve_magnitude' , kwargs , _nyquist_defaults , pop = True )
713+ max_curve_offset = config ._get_param (
714+ 'nyquist' , 'max_curve_offset' , kwargs , _nyquist_defaults , pop = True )
715+
716+ # Set line styles for the curves
717+ def _parse_linestyle (style_name , allow_false = False ):
718+ style = config ._get_param (
719+ 'nyquist' , style_name , kwargs , _nyquist_defaults , pop = True )
720+ if isinstance (style , str ):
721+ # Only one style provided, use the default for the other
722+ style = [style , _nyquist_defaults ['nyquist.' + style_name ][1 ]]
723+ if (allow_false and style is False ) or \
724+ (isinstance (style , list ) and len (style ) == 2 ):
725+ return style
726+ else :
727+ raise ValueError (f"invalid '{ style_name } ': { style } " )
728+
729+ primary_style = _parse_linestyle ('primary_style' )
730+ mirror_style = _parse_linestyle ('mirror_style' , allow_false = True )
682731
683732 # If argument was a singleton, turn it into a tuple
684733 if not isinstance (syslist , (list , tuple )):
@@ -759,15 +808,39 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
759808 "consider reducing indent_radius to be less than "
760809 f"{ abs (p_ol - p_cl ):5.2g} " , stacklevel = 2 )
761810
762- # See if we should add some frequency points near the origin
763- if splane_contour [1 ].imag > indent_radius \
764- and np .any (np .isclose (abs (splane_poles ), 0 )) \
765- and not omega_range_given :
766- # add some points for quarter circle around poles at origin
811+ #
812+ # See if we should add some frequency points near imaginary poles
813+ #
814+ for p in splane_poles :
815+ # See if we need to process this pole (skip any that is on
816+ # the not near or on the negative omega axis + user override)
817+ if p .imag < 0 or abs (p .real ) > indent_radius or \
818+ omega_range_given :
819+ continue
820+
821+ # Find the frequencies before the pole frequency
822+ below_points = np .argwhere (
823+ splane_contour .imag - abs (p .imag ) < - indent_radius )
824+ if below_points .size > 0 :
825+ first_point = below_points [- 1 ].item ()
826+ start_freq = p .imag - indent_radius
827+ else :
828+ # Add the points starting at the beginning of the contour
829+ assert splane_contour [0 ] == 0
830+ first_point = 0
831+ start_freq = 0
832+
833+ above_points = np .argwhere (
834+ splane_contour .imag - abs (p .imag ) > indent_radius )
835+ last_point = above_points [0 ].item ()
836+
837+ # Add points for half/quarter circle around pole frequency
767838 # (these will get indented left or right below)
768- splane_contour = np .concatenate (
769- (1j * np .linspace (0. , indent_radius , 50 ),
770- splane_contour [1 :]))
839+ splane_contour = np .concatenate ((
840+ splane_contour [0 :first_point + 1 ],
841+ (1j * np .linspace (
842+ start_freq , p .imag + indent_radius , indent_points )),
843+ splane_contour [last_point :]))
771844
772845 for i , s in enumerate (splane_contour ):
773846 # Find the nearest pole
@@ -849,19 +922,55 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
849922 arrow_style = mpl .patches .ArrowStyle (
850923 'simple' , head_width = arrow_size , head_length = arrow_size )
851924
852- # Save the components of the response
853- x , y = resp .real , resp .imag
925+ # Find the different portions of the curve (with scaled pts marked)
926+ reg_mask = abs (resp ) > max_curve_magnitude
927+ scale_mask = ~ reg_mask \
928+ & np .concatenate ((~ reg_mask [1 :], ~ reg_mask [- 1 :])) \
929+ & np .concatenate ((~ reg_mask [0 :1 ], ~ reg_mask [:- 1 ]))
930+
931+ # Rescale the points with large magnitude
932+ resp [reg_mask ] /= (np .abs (resp [reg_mask ]) / max_curve_magnitude )
854933
855- # Plot the primary curve
856- p = plt .plot (x , y , '-' , color = color , * args , ** kwargs )
934+ # Plot the regular portions of the curve (and grab the color)
935+ x_reg = np .ma .masked_where (reg_mask , resp .real )
936+ y_reg = np .ma .masked_where (reg_mask , resp .imag )
937+ p = plt .plot (
938+ x_reg , y_reg , primary_style [0 ], color = color , * args , ** kwargs )
857939 c = p [0 ].get_color ()
940+
941+ # Plot the scaled sections of the curve (changing linestyle)
942+ x_scl = np .ma .masked_where (scale_mask , resp .real )
943+ y_scl = np .ma .masked_where (scale_mask , resp .imag )
944+ plt .plot (
945+ x_scl * (1 + max_curve_offset ), y_scl * (1 + max_curve_offset ),
946+ primary_style [1 ], color = c , * args , ** kwargs )
947+
948+ # Plot the primary curve (invisible) for setting arrows
949+ x , y = resp .real .copy (), resp .imag .copy ()
950+ x [reg_mask ] *= (1 + max_curve_offset )
951+ y [reg_mask ] *= (1 + max_curve_offset )
952+ p = plt .plot (x , y , linestyle = 'None' , color = c , * args , ** kwargs )
953+
954+ # Add arrows
858955 ax = plt .gca ()
859956 _add_arrows_to_line2D (
860957 ax , p [0 ], arrow_pos , arrowstyle = arrow_style , dir = 1 )
861958
862959 # Plot the mirror image
863960 if mirror_style is not False :
864- p = plt .plot (x , - y , mirror_style , color = c , * args , ** kwargs )
961+ # Plot the regular and scaled segments
962+ plt .plot (
963+ x_reg , - y_reg , mirror_style [0 ], color = c , * args , ** kwargs )
964+ plt .plot (
965+ x_scl * (1 - max_curve_offset ),
966+ - y_scl * (1 - max_curve_offset ),
967+ mirror_style [1 ], color = c , * args , ** kwargs )
968+
969+ # Add the arrows (on top of an invisible contour)
970+ x , y = resp .real .copy (), resp .imag .copy ()
971+ x [reg_mask ] *= (1 - max_curve_offset )
972+ y [reg_mask ] *= (1 - max_curve_offset )
973+ p = plt .plot (x , - y , linestyle = 'None' , color = c , * args , ** kwargs )
865974 _add_arrows_to_line2D (
866975 ax , p [0 ], arrow_pos , arrowstyle = arrow_style , dir = - 1 )
867976
@@ -982,7 +1091,6 @@ def _add_arrows_to_line2D(
9821091#
9831092# Gang of Four plot
9841093#
985-
9861094# TODO: think about how (and whether) to handle lists of systems
9871095def gangof4_plot (P , C , omega = None , ** kwargs ):
9881096 """Plot the "Gang of 4" transfer functions for a system
0 commit comments