Skip to content

Commit 6ce29da

Browse files
committed
TMP: update margins processing (display_margins)
1 parent 2d6a779 commit 6ce29da

4 files changed

Lines changed: 80 additions & 82 deletions

File tree

control/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,13 @@ def use_legacy_defaults(version):
326326
#
327327
# Use this function to handle a legacy keyword that has been renamed. This
328328
# function pops the old keyword off of the kwargs dictionary and issues a
329-
# warning. if both the old and new keyword are present, a ControlArgument
329+
# warning. If both the old and new keyword are present, a ControlArgument
330330
# exception is raised.
331331
#
332332
def _process_legacy_keyword(kwargs, oldkey, newkey, newval):
333333
if kwargs.get(oldkey) is not None:
334334
warnings.warn(
335-
f"keyworld '{oldkey}' is deprecated; use '{newkey}'",
335+
f"keyword '{oldkey}' is deprecated; use '{newkey}'",
336336
DeprecationWarning)
337337
if newval is not None:
338338
raise ControlArgument(

control/freqplot.py

Lines changed: 68 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,10 @@ def plot(self, *args, plot_type=None, **kwargs):
149149

150150
def 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):

control/sisotool.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None,
112112
'omega_limits': omega_limits,
113113
'omega_num' : omega_num,
114114
'ax': axes[:, 0:1],
115-
'margins': margins_bode,
116-
'margin_info': True,
115+
'display_margins': 'overlay' if margins_bode else False,
117116
}
118117

119118
# Check to see if legacy 'PrintGain' keyword was used

control/tests/freqresp_test.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,27 +204,28 @@ def test_bode_margin(dB, maginfty1, maginfty2, gminv,
204204
fig = plt.gcf()
205205
allaxes = fig.get_axes()
206206

207+
# TODO: update with better tests for new margin plots
207208
mag_to_infinity = (np.array([Wcp, Wcp]),
208209
np.array([maginfty1, maginfty2]))
209-
assert_allclose(mag_to_infinity,
210-
allaxes[0].lines[2].get_data(),
210+
assert_allclose(mag_to_infinity[0],
211+
allaxes[0].lines[2].get_data()[0],
211212
rtol=1e-5)
212213

213214
gm_to_infinty = (np.array([Wcg, Wcg]),
214215
np.array([gminv, maginfty2]))
215-
assert_allclose(gm_to_infinty,
216-
allaxes[0].lines[3].get_data(),
216+
assert_allclose(gm_to_infinty[0],
217+
allaxes[0].lines[3].get_data()[0],
217218
rtol=1e-5)
218219

219220
one_to_gm = (np.array([Wcg, Wcg]),
220221
np.array([maginfty1, gminv]))
221-
assert_allclose(one_to_gm, allaxes[0].lines[4].get_data(),
222+
assert_allclose(one_to_gm[0], allaxes[0].lines[4].get_data()[0],
222223
rtol=1e-5)
223224

224225
pm_to_infinity = (np.array([Wcp, Wcp]),
225226
np.array([1e5, pm]))
226-
assert_allclose(pm_to_infinity,
227-
allaxes[1].lines[2].get_data(),
227+
assert_allclose(pm_to_infinity[0],
228+
allaxes[1].lines[2].get_data()[0],
228229
rtol=1e-5)
229230

230231
pm_to_phase = (np.array([Wcp, Wcp]),
@@ -234,7 +235,7 @@ def test_bode_margin(dB, maginfty1, maginfty2, gminv,
234235

235236
phase_to_infinity = (np.array([Wcg, Wcg]),
236237
np.array([0, p0]))
237-
assert_allclose(phase_to_infinity, allaxes[1].lines[4].get_data(),
238+
assert_allclose(phase_to_infinity[0], allaxes[1].lines[4].get_data()[0],
238239
rtol=1e-5)
239240

240241

0 commit comments

Comments
 (0)