Replacing deprecated use of pyplot.subplot

Hi,

I'm a contributor to the Python Control Systems Library [1], which uses
Matplotlib for plotting.

We recently noticed deprecation warnings due to how we use
pyplot.subplot. We use it in the Matlab manner of either getting a
handle to an existing axis, or creating one if no suitable axis exists.

The warning is

  MatplotlibDeprecationWarning: Adding an axes using the same arguments
  as a previous axes currently reuses the earlier instance. In a future
  version, a new instance will always be created and returned.
  Meanwhile, this warning can be suppressed, and the future behavior
  ensured, by passing a unique label to each axes instance.

For example, to plot the frequency response a linear dynamical system
(AKA Bode plot of the system), the relevant function could be simplified
to:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)
       subplot(211)
       semilogx(freq, 20*log10(mag))
       subplot(212)
       semilogx(freq, phase)

We've replaced that with code like this:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)

       ax_mag = None
       ax_phase = None
       for ax in gcf.axes():
           if ax.get_label() == 'control-bode-magnitude':
             ax_mag = ax
           elif ax.get_label() == 'control-bode-phase':
             ax_phase = ax

       if ax_mag is None or ax_phase is None:
         clf()
         ax_mag = subplot(211, label = 'control-bode-magnitude')
         ax_phase = subplot(212, label = 'control-bode-phase)

       ax_mag.semilogx(freq, 20*log10(mag))
       ax_phase.semilogx(freq, phase)
       
This means that calls like

    bode_plot(g)
    bode_plot(h)

will show the response of g and h on the same figure.

Is this method of using labels to check for existing axes reasonable?
Is there a better way?

Actual code exhibiting warnings at [2]; new code at [3]. The latter
link is to an as-yet unmerged branch, and may disappear.

Thanks,

Rory

[1] https://github.com/python-control/python-control
[2] https://github.com/python-control/python-control/blob/af8d4ee39dfa574c2b3b335f4cdb4be858ae469a/control/freqplot.py#L175
[3] https://github.com/murrayrm/python-control/blob/dc1820a4e64d73937c7de8df078c41ec1773e048/control/freqplot.py#L138

Hey Rory,

In general, especially for library code, you should avoid relying on the
pyplot state machine.

That means explicitly passing Axes and Figure object around to and from
your functions.

For me, making that switch meant added an `ax=None` kwarg to the end of my
function signatures.

Then I carry around an axes_validator function that looks something like
this:
https://github.com/matplotlib/mpl-probscale/blob/master/probscale/validate.py#L6

But that's not really necessary. So in your case, I think you should do
something like this:

import numpy as np
import matplotlib.pyplot as plt

def bode_plot(g, ax_mag=None, ax_phase=None, mag_opts=None,
phase_opts=None):
    # create axes if they're not both supplied
    if not ax_mag or not ax_phase:
        fig, (ax_mag, ax_phase) = plt.subplots(nrows=2)

    # make the plotting options empty dicts if
    # not supplied
    if not mag_opts:
        mag_opts = {}

    if not phase_opts:
        phase_opts = {}

    # compute signal stuff
    freq, mag, phase = freq_resp(g)

    # 99% sure semilogx returns a tuple of artists, so I unpack it
    # you should check this though
    mag_artist, = ax_mag.semilogx(freq, 20 * np.log10(mag), **mag_opts)
    phase_artist, = ax_phase.semilogx(freq, phase, **phase_opts)

    # package the output for later (if you want to modify artists)
    output = {
        'fig': fig,
        'axes': (ax_mag, ax_phase),
        'arists': (mag_artist, phase_artist)
    }
    return output

And then you'd use the code like this:

fig, (ax_mag, ax_phase) = plt.subplots(nrows=2, figsize=(12, 6))
g_mpl = bode_plot(g, ax_mag=ax_mag, ax_phase=ax_phase, color='r',
linewidth=2, label='G')
h_mpl = bode_plot(h, ax_mag=ax_mag, ax_phase=ax_phase, color='b'm
linewidth=1, label='H')
ax_mag.legend()

Or do something like this:
g_mpl = bode_plot(g, color='r', linewidth=2, label='G')
h_mpl = bode_plot(h, ax_mag=g_mpl['axes'][0], ax_phase=g_mpl['axes'][0])

Does that help?
-Paul

···

On Sat, Jan 20, 2018 at 11:44 AM, Rory Yorke <rory.yorke at gmail.com> wrote:

Hi,

I'm a contributor to the Python Control Systems Library [1], which uses
Matplotlib for plotting.

We recently noticed deprecation warnings due to how we use
pyplot.subplot. We use it in the Matlab manner of either getting a
handle to an existing axis, or creating one if no suitable axis exists.

The warning is

  MatplotlibDeprecationWarning: Adding an axes using the same arguments
  as a previous axes currently reuses the earlier instance. In a future
  version, a new instance will always be created and returned.
  Meanwhile, this warning can be suppressed, and the future behavior
  ensured, by passing a unique label to each axes instance.

For example, to plot the frequency response a linear dynamical system
(AKA Bode plot of the system), the relevant function could be simplified
to:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)
       subplot(211)
       semilogx(freq, 20*log10(mag))
       subplot(212)
       semilogx(freq, phase)

We've replaced that with code like this:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)

       ax_mag = None
       ax_phase = None
       for ax in gcf.axes():
           if ax.get_label() == 'control-bode-magnitude':
             ax_mag = ax
           elif ax.get_label() == 'control-bode-phase':
             ax_phase = ax

       if ax_mag is None or ax_phase is None:
         clf()
         ax_mag = subplot(211, label = 'control-bode-magnitude')
         ax_phase = subplot(212, label = 'control-bode-phase)

       ax_mag.semilogx(freq, 20*log10(mag))
       ax_phase.semilogx(freq, phase)

This means that calls like

    bode_plot(g)
    bode_plot(h)

will show the response of g and h on the same figure.

Is this method of using labels to check for existing axes reasonable?
Is there a better way?

Actual code exhibiting warnings at [2]; new code at [3]. The latter
link is to an as-yet unmerged branch, and may disappear.

Thanks,

Rory

[1] https://github.com/python-control/python-control
[2] https://github.com/python-control/python-control/blob/
af8d4ee39dfa574c2b3b335f4cdb4be858ae469a/control/freqplot.py#L175
[3] https://github.com/murrayrm/python-control/blob/
dc1820a4e64d73937c7de8df078c41ec1773e048/control/freqplot.py#L138
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users at python.org
https://mail.python.org/mailman/listinfo/matplotlib-users

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-users/attachments/20180120/495750c2/attachment.html>

Errr, make that last line:

h_mpl = bode_plot(h, ax_mag=g_mpl['axes'][0], ax_phase=g_mpl['axes']*[1]*)

···

On Sat, Jan 20, 2018 at 1:02 PM, Paul Hobson <pmhobson at gmail.com> wrote:

Hey Rory,

In general, especially for library code, you should avoid relying on the
pyplot state machine.

That means explicitly passing Axes and Figure object around to and from
your functions.

For me, making that switch meant added an `ax=None` kwarg to the end of my
function signatures.

Then I carry around an axes_validator function that looks something like
this:
https://github.com/matplotlib/mpl-probscale/blob/master/
probscale/validate.py#L6

But that's not really necessary. So in your case, I think you should do
something like this:

import numpy as np
import matplotlib.pyplot as plt

def bode_plot(g, ax_mag=None, ax_phase=None, mag_opts=None,
phase_opts=None):
    # create axes if they're not both supplied
    if not ax_mag or not ax_phase:
        fig, (ax_mag, ax_phase) = plt.subplots(nrows=2)

    # make the plotting options empty dicts if
    # not supplied
    if not mag_opts:
        mag_opts = {}

    if not phase_opts:
        phase_opts = {}

    # compute signal stuff
    freq, mag, phase = freq_resp(g)

    # 99% sure semilogx returns a tuple of artists, so I unpack it
    # you should check this though
    mag_artist, = ax_mag.semilogx(freq, 20 * np.log10(mag), **mag_opts)
    phase_artist, = ax_phase.semilogx(freq, phase, **phase_opts)

    # package the output for later (if you want to modify artists)
    output = {
        'fig': fig,
        'axes': (ax_mag, ax_phase),
        'arists': (mag_artist, phase_artist)
    }
    return output

And then you'd use the code like this:

fig, (ax_mag, ax_phase) = plt.subplots(nrows=2, figsize=(12, 6))
g_mpl = bode_plot(g, ax_mag=ax_mag, ax_phase=ax_phase, color='r',
linewidth=2, label='G')
h_mpl = bode_plot(h, ax_mag=ax_mag, ax_phase=ax_phase, color='b'm
linewidth=1, label='H')
ax_mag.legend()

Or do something like this:
g_mpl = bode_plot(g, color='r', linewidth=2, label='G')
h_mpl = bode_plot(h, ax_mag=g_mpl['axes'][0], ax_phase=g_mpl['axes'][0])

Does that help?
-Paul

On Sat, Jan 20, 2018 at 11:44 AM, Rory Yorke <rory.yorke at gmail.com> wrote:

Hi,

I'm a contributor to the Python Control Systems Library [1], which uses
Matplotlib for plotting.

We recently noticed deprecation warnings due to how we use
pyplot.subplot. We use it in the Matlab manner of either getting a
handle to an existing axis, or creating one if no suitable axis exists.

The warning is

  MatplotlibDeprecationWarning: Adding an axes using the same arguments
  as a previous axes currently reuses the earlier instance. In a future
  version, a new instance will always be created and returned.
  Meanwhile, this warning can be suppressed, and the future behavior
  ensured, by passing a unique label to each axes instance.

For example, to plot the frequency response a linear dynamical system
(AKA Bode plot of the system), the relevant function could be simplified
to:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)
       subplot(211)
       semilogx(freq, 20*log10(mag))
       subplot(212)
       semilogx(freq, phase)

We've replaced that with code like this:

    def bode_plot(g):
       freq, mag, phase = freq_resp(g)

       ax_mag = None
       ax_phase = None
       for ax in gcf.axes():
           if ax.get_label() == 'control-bode-magnitude':
             ax_mag = ax
           elif ax.get_label() == 'control-bode-phase':
             ax_phase = ax

       if ax_mag is None or ax_phase is None:
         clf()
         ax_mag = subplot(211, label = 'control-bode-magnitude')
         ax_phase = subplot(212, label = 'control-bode-phase)

       ax_mag.semilogx(freq, 20*log10(mag))
       ax_phase.semilogx(freq, phase)

This means that calls like

    bode_plot(g)
    bode_plot(h)

will show the response of g and h on the same figure.

Is this method of using labels to check for existing axes reasonable?
Is there a better way?

Actual code exhibiting warnings at [2]; new code at [3]. The latter
link is to an as-yet unmerged branch, and may disappear.

Thanks,

Rory

[1] https://github.com/python-control/python-control
[2] https://github.com/python-control/python-control/blob/af8d4e
e39dfa574c2b3b335f4cdb4be858ae469a/control/freqplot.py#L175
[3] https://github.com/murrayrm/python-control/blob/dc1820a4e64d
73937c7de8df078c41ec1773e048/control/freqplot.py#L138
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users at python.org
https://mail.python.org/mailman/listinfo/matplotlib-users

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-users/attachments/20180120/2d0c53d9/attachment-0001.html>

Rory,

The general direction of Matplotlib's evolution is toward encouraging
more explicit code, so that the programmer or user has more
responsibility for specifying which figure and axes are to be used,
rather than relying on the state machine and the concepts of "current
figure" and "current axes". Therefore we recommend using pyplot
functions very sparingly.

If you need to keep your API exactly as it is, your approach using
labels looks reasonable. I think it can be simplified, though, by
defining a helper function something like this:

def _get_bode_axes():
     fig = plt.gcf()
     if not hasattr(fig, '_bode_axes'):
         fig.clf()
         fig._bode_axes = fig.subplots(2, 1, sharex=True)
     return fig._bode_axes

Then, outside any loop in plot_bode but conditional on the Plot kwarg,
use a single call:

     ax_mag, ax_phase = _get_bode_axes()

Also conditional on Plot, put your loop over syslist to do the plotting.
  I would make that loop separate from the calculation. In general,
code is clearer and easier to test when calculations are separated from
plotting, ideally with separate functions.

You could also use the initialization block inside _get_bode_axes to
customize the axes with respect to grid, labels...anything that you
don't want to change as you add lines to the plot, and that you can set
once with the first call to plot_bode and won't potentially need to
change in subsequent calls that write to the same figure. Or you could
do that sort of customization outside and after the loop that plots the
lines.

Eric

···

On 2018/01/20 9:44 AM, Rory Yorke wrote:

Hi,

I'm a contributor to the Python Control Systems Library [1], which uses
Matplotlib for plotting.

We recently noticed deprecation warnings due to how we use
pyplot.subplot. We use it in the Matlab manner of either getting a
handle to an existing axis, or creating one if no suitable axis exists.

The warning is

   MatplotlibDeprecationWarning: Adding an axes using the same arguments
   as a previous axes currently reuses the earlier instance. In a future
   version, a new instance will always be created and returned.
   Meanwhile, this warning can be suppressed, and the future behavior
   ensured, by passing a unique label to each axes instance.

For example, to plot the frequency response a linear dynamical system
(AKA Bode plot of the system), the relevant function could be simplified
to:

     def bode_plot(g):
        freq, mag, phase = freq_resp(g)
        subplot(211)
        semilogx(freq, 20*log10(mag))
        subplot(212)
        semilogx(freq, phase)

We've replaced that with code like this:

     def bode_plot(g):
        freq, mag, phase = freq_resp(g)

        ax_mag = None
        ax_phase = None
        for ax in gcf.axes():
            if ax.get_label() == 'control-bode-magnitude':
              ax_mag = ax
            elif ax.get_label() == 'control-bode-phase':
              ax_phase = ax

        if ax_mag is None or ax_phase is None:
          clf()
          ax_mag = subplot(211, label = 'control-bode-magnitude')
          ax_phase = subplot(212, label = 'control-bode-phase)

        ax_mag.semilogx(freq, 20*log10(mag))
        ax_phase.semilogx(freq, phase)
        
This means that calls like

     bode_plot(g)
     bode_plot(h)

will show the response of g and h on the same figure.

Is this method of using labels to check for existing axes reasonable?
Is there a better way?

Actual code exhibiting warnings at [2]; new code at [3]. The latter
link is to an as-yet unmerged branch, and may disappear.

Thanks,

Rory

[1] https://github.com/python-control/python-control
[2] https://github.com/python-control/python-control/blob/af8d4ee39dfa574c2b3b335f4cdb4be858ae469a/control/freqplot.py#L175
[3] https://github.com/murrayrm/python-control/blob/dc1820a4e64d73937c7de8df078c41ec1773e048/control/freqplot.py#L138
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users at python.org
https://mail.python.org/mailman/listinfo/matplotlib-users

Hi Paul,

Paul Hobson <pmhobson at gmail.com> writes:

In general, especially for library code, you should avoid relying on the
pyplot state machine.

OK, thanks. Eric said much the same in his reply.

Thanks for the example code, that helps. It seems like the recommended
overall approach is "if axes are provided, used them, else create new ones;
return the axes used".

For now we'll use the approach I've implemented, perhaps with Eric's
suggested improvement. I'll propose creating a new suite of plot
functions (there are several specialized plots we do: Bode, Nichols,
etc.) that adopt the no-state-machine approach.

Thanks, both to you and Eric.

Regards,

Rory