@@ -149,10 +149,10 @@ def plot(self, *args, plot_type=None, **kwargs):
149149
150150def bode_plot (
151151 data , omega = None , * fmt , ax = None , omega_limits = None , omega_num = None ,
152- plot = None , plot_magnitude = True , plot_phase = None , margins = None ,
152+ plot = None , plot_magnitude = True , plot_phase = None ,
153153 overlay_outputs = None , overlay_inputs = None , phase_label = None ,
154- magnitude_label = None ,
155- margin_info = False , method = 'best' , legend_map = None , legend_loc = None ,
154+ magnitude_label = None , display_margins = None ,
155+ margins_method = 'best' , legend_map = None , legend_loc = None ,
156156 sharex = None , sharey = None , title = None , relabel = True , ** kwargs ):
157157 """Bode plot for a system.
158158
@@ -173,11 +173,12 @@ def bode_plot(
173173 deg : bool
174174 If True, plot phase in degrees (else radians). Default value (True)
175175 set by config.defaults['freqplot.deg'].
176- margins : bool
177- If True, plot gain and phase margin. (TODO: merge with margin_info)
178- margin_info : bool
179- If True, plot information about gain and phase margin.
180- method : str, optional
176+ display_margins : bool or str
177+ If True, draw gain and phase margin lines on the magnitude and phase
178+ graphs and display the margins at the top of the graph. If set to
179+ 'overlay', the values for the gain and phase margin are placed on
180+ the graph. Setting display_margins turns off the axes grid.
181+ margins_method : str, optional
181182 Method to use in computing margins (see :func:`stability_margins`).
182183 *fmt : :func:`matplotlib.pyplot.plot` format string, optional
183184 Passed to `matplotlib` as the format string for all lines in the plot.
@@ -269,8 +270,6 @@ def bode_plot(
269270 'freqplot' , 'Hz' , kwargs , _freqplot_defaults , pop = True )
270271 grid = config ._get_param (
271272 'freqplot' , 'grid' , kwargs , _freqplot_defaults , pop = True )
272- margins = config ._get_param (
273- 'freqplot' , 'margins' , margins , False )
274273 wrap_phase = config ._get_param (
275274 'freqplot' , 'wrap_phase' , kwargs , _freqplot_defaults , pop = True )
276275 initial_phase = config ._get_param (
@@ -300,6 +299,19 @@ def bode_plot(
300299 "sharex cannot be present with share_frequency" )
301300 kwargs ['share_frequency' ] = sharex
302301
302+ # Legacy keywords for margins
303+ display_margins = config ._process_legacy_keyword (
304+ kwargs , 'margins' , 'display_margins' , display_margins )
305+ if kwargs .pop ('margin_info' , False ):
306+ warnings .warn (
307+ "keyword 'margin_info' is deprecated; "
308+ "use 'display_margins='overlay'" )
309+ if display_margins is False :
310+ raise ValueError (
311+ "conflicting_keywords: `display_margins` and `margin_info`" )
312+ margins_method = config ._process_legacy_keyword (
313+ kwargs , 'method' , 'margins_method' , margins_method )
314+
303315 if not isinstance (data , (list , tuple )):
304316 data = [data ]
305317
@@ -725,8 +737,8 @@ def _make_line_label(response, output_index, input_index):
725737 nyq_freq , color = lines [0 ].get_color (), linestyle = '--' ,
726738 label = '_nyq_mag_' + sysname )
727739
728- # Add a grid to the plot + labeling (TODO? move to later?)
729- ax_mag .grid (grid and not margins , which = 'both' )
740+ # Add a grid to the plot
741+ ax_mag .grid (grid and not display_margins , which = 'both' )
730742
731743 # Phase
732744 if plot_phase :
@@ -740,22 +752,22 @@ def _make_line_label(response, output_index, input_index):
740752 nyq_freq , color = lines [0 ].get_color (), linestyle = '--' ,
741753 label = '_nyq_phase_' + sysname )
742754
743- # Add a grid to the plot + labeling
744- ax_phase .grid (grid and not margins , which = 'both' )
755+ # Add a grid to the plot
756+ ax_phase .grid (grid and not display_margins , which = 'both' )
757+ print (f"phase_ylim={ ax_phase .get_ylim ()} " )
745758
746759 #
747- # Plot gain and phase margins (SISO only)
760+ # Display gain and phase margins (SISO only)
748761 #
749762
750- # Show the phase and gain margins in the plot
751- if margins :
763+ if display_margins :
752764 if ninputs > 1 or noutputs > 1 :
753765 raise NotImplementedError (
754766 "margins are not available for MIMO systems" )
755767
756768 # Compute stability margins for the system
757- margin = stability_margins (response , method = method )
758- gm , pm , Wcg , Wcp = (margin [i ] for i in [0 , 1 , 3 , 4 ])
769+ margins = stability_margins (response , method = margins_method )
770+ gm , pm , Wcg , Wcp = (margins [i ] for i in [0 , 1 , 3 , 4 ])
759771
760772 # Figure out sign of the phase at the first gain crossing
761773 # (needed if phase_wrap is True)
@@ -780,69 +792,47 @@ def _make_line_label(response, output_index, input_index):
780792 math .radians (phase_limit ),
781793 color = 'k' , linestyle = ':' , zorder = - 20 )
782794 phase_ylim = ax_phase .get_ylim ()
795+ print (f"{ phase_ylim = } " )
783796
784797 # Annotate the phase margin (if it exists)
785798 if plot_phase and pm != float ('inf' ) and Wcp != float ('nan' ):
786- if dB :
787- ax_mag .semilogx (
788- [Wcp , Wcp ], [0. , - 1e5 ],
789- color = 'k' , linestyle = ':' , zorder = - 20 )
790- else :
791- ax_mag .loglog (
792- [Wcp , Wcp ], [1. , 1e-8 ],
793- color = 'k' , linestyle = ':' , zorder = - 20 )
799+ # Draw dotted lines marking the gain crossover frequencies
800+ if plot_magnitude :
801+ ax_mag .axvline (Wcp , color = 'k' , linestyle = ':' , zorder = - 30 )
802+ ax_phase .axvline (Wcp , color = 'k' , linestyle = ':' , zorder = - 30 )
794803
804+ # Draw solid segments indicating the margins
795805 if deg :
796- ax_phase .semilogx (
797- [Wcp , Wcp ], [1e5 , phase_limit + pm ],
798- color = 'k' , linestyle = ':' , zorder = - 20 )
799806 ax_phase .semilogx (
800807 [Wcp , Wcp ], [phase_limit + pm , phase_limit ],
801808 color = 'k' , zorder = - 20 )
802809 else :
803- ax_phase .semilogx (
804- [Wcp , Wcp ], [1e5 , math .radians (phase_limit ) +
805- math .radians (pm )],
806- color = 'k' , linestyle = ':' , zorder = - 20 )
807810 ax_phase .semilogx (
808811 [Wcp , Wcp ], [math .radians (phase_limit ) +
809812 math .radians (pm ),
810813 math .radians (phase_limit )],
811814 color = 'k' , zorder = - 20 )
812815
813- ax_phase .set_ylim (phase_ylim )
814-
815816 # Annotate the gain margin (if it exists)
816817 if plot_magnitude and gm != float ('inf' ) and \
817818 Wcg != float ('nan' ):
819+ # Draw dotted lines marking the phase crossover frequencies
820+ ax_mag .axvline (Wcg , color = 'k' , linestyle = ':' , zorder = - 30 )
821+ if plot_phase :
822+ ax_phase .axvline (Wcg , color = 'k' , linestyle = ':' , zorder = - 30 )
823+
824+ # Draw solid segments indicating the margins
818825 if dB :
819- ax_mag .semilogx (
820- [Wcg , Wcg ], [- 20. * np .log10 (gm ), - 1e5 ],
821- color = 'k' , linestyle = ':' , zorder = - 20 )
822826 ax_mag .semilogx (
823827 [Wcg , Wcg ], [0 , - 20 * np .log10 (gm )],
824828 color = 'k' , zorder = - 20 )
825829 else :
826- ax_mag .loglog (
827- [Wcg , Wcg ], [1. / gm , 1e-8 ], color = 'k' ,
828- linestyle = ':' , zorder = - 20 )
829830 ax_mag .loglog (
830831 [Wcg , Wcg ], [1. , 1. / gm ], color = 'k' , zorder = - 20 )
831832
832- if plot_phase :
833- if deg :
834- ax_phase .semilogx (
835- [Wcg , Wcg ], [0 , phase_limit ],
836- color = 'k' , linestyle = ':' , zorder = - 20 )
837- else :
838- ax_phase .semilogx (
839- [Wcg , Wcg ], [0 , math .radians (phase_limit )],
840- color = 'k' , linestyle = ':' , zorder = - 20 )
841-
842- ax_mag .set_ylim (mag_ylim )
843- ax_phase .set_ylim (phase_ylim )
844-
845- if margin_info :
833+ if display_margins == 'overlay' :
834+ # TODO: figure out how to handle case of multiple lines
835+ # Put the margin information in the lower left corner
846836 if plot_magnitude :
847837 ax_mag .text (
848838 0.04 , 0.06 ,
@@ -854,6 +844,7 @@ def _make_line_label(response, output_index, input_index):
854844 verticalalignment = 'bottom' ,
855845 transform = ax_mag .transAxes ,
856846 fontsize = 8 if int (mpl .__version__ [0 ]) == 1 else 6 )
847+
857848 if plot_phase :
858849 ax_phase .text (
859850 0.04 , 0.06 ,
@@ -865,17 +856,24 @@ def _make_line_label(response, output_index, input_index):
865856 verticalalignment = 'bottom' ,
866857 transform = ax_phase .transAxes ,
867858 fontsize = 8 if int (mpl .__version__ [0 ]) == 1 else 6 )
859+
868860 else :
869- # TODO: gets overwritten below
870- plt .suptitle (
871- "Gm = %.2f %s(at %.2f %s), "
872- "Pm = %.2f %s (at %.2f %s)" %
873- (20 * np .log10 (gm ) if dB else gm ,
874- 'dB ' if dB else '' ,
875- Wcg , 'Hz' if Hz else 'rad/s' ,
876- pm if deg else math .radians (pm ),
877- 'deg' if deg else 'rad' ,
878- Wcp , 'Hz' if Hz else 'rad/s' ))
861+ # Put the title underneath the suptitle (one line per system)
862+ ax = ax_mag if ax_mag else ax_phase
863+ axes_title = ax .get_title ()
864+ if axes_title is not None and axes_title != "" :
865+ axes_title += "\n "
866+ with plt .rc_context (_freqplot_rcParams ):
867+ ax .set_title (
868+ axes_title + f"{ sysname } : "
869+ "Gm = %.2f %s(at %.2f %s), "
870+ "Pm = %.2f %s (at %.2f %s)" %
871+ (20 * np .log10 (gm ) if dB else gm ,
872+ 'dB ' if dB else '' ,
873+ Wcg , 'Hz' if Hz else 'rad/s' ,
874+ pm if deg else math .radians (pm ),
875+ 'deg' if deg else 'rad' ,
876+ Wcp , 'Hz' if Hz else 'rad/s' ))
879877
880878 #
881879 # Finishing handling axes limit sharing
@@ -887,12 +885,12 @@ def _make_line_label(response, output_index, input_index):
887885 # * manually generated labels and grids need to reflect the limts for
888886 # shared axes, which we don't know until we have plotted everything;
889887 #
890- # * the use of loglog and semilog regenerate the labels (not quite sure
891- # why, since using sharex and sharey in subplots does not have this
892- # behavior).
888+ # * the loglog and semilog functions regenerate the labels (not quite
889+ # sure why, since using sharex and sharey in subplots does not have
890+ # this behavior).
893891 #
894892 # Note: as before, if the various share_* keywords are None then a
895- # previous set of axes are available and no updates are made.
893+ # previous set of axes are available and no updates are made. (TODO: true?)
896894 #
897895
898896 for i in range (noutputs ):
0 commit comments