How do I register the reciprocal (1/x) scale in Matplotlib?

Question:

Currently, I get this plot from my Python script:

enter image description here

The frequency-period relation is not linear, it’s reciprocal, being period = 1./frequency or, if expressed as a power, period = frequency**(-1). The auxiliary x-axis scale is linear in my script, that’s why the red dashed-line, based on the frequency ~0.123365408 Hz, shows the wrong period, it should show ~8.106 s, but it’s showing ~8.155 s. So, I need to register a new scale for the auxiliary x-axis, but I dont know how to do that.

I’ve found a similar problem here:
Square root scale using matplotlib/python

It’s basically the same, because the root scale is a power of 0.5, while the reciprocal scale is a power of (-1). So, I’ve tried that solution replacing both

return np.array(a)**0.5

and

return np.array(a)**2

with

return np.array(a)**(-1)

and renaming SquareRoot with Reciprocal.
But that didnt help me, I am getting something weird on my auxiliary x-axis (no ticks, only one tick label). I guess, I also have to handle the ticks of the new scale. Then, I’ve found this issue:
python 1/x plot scale formatting, tick position

But I dont understand how to merge the both solutions. Could someone help me, please? Thanks in advance!

This is the relevant code excerpt:

import matplotlib.pyplot as plt
import matplotlib.scale as mscale
import matplotlib.transforms as mtransforms
import matplotlib.ticker as ticker

class ReciprocalScale(mscale.ScaleBase):
    """
    ScaleBase class for generating reciprocal scale.
    """

    name = 'reciprocal'

    def __init__(self, axis, **kwargs):
        mscale.ScaleBase.__init__(self)

    def set_default_locators_and_formatters(self, axis):
        axis.set_major_locator(ticker.AutoLocator())
        axis.set_major_formatter(ticker.ScalarFormatter())
        axis.set_minor_locator(ticker.NullLocator())
        axis.set_minor_formatter(ticker.NullFormatter())

    def limit_range_for_scale(self, vmin, vmax, minpos):
        return  max(0., vmin), vmax

    class ReciprocalTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True

        def transform_non_affine(self, a): 
            return np.array(a)**(-1)

        def inverted(self):
            return ReciprocalScale.InvertedReciprocalTransform()

    class InvertedReciprocalTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True

        def transform(self, a):
            return np.array(a)**(-1)

        def inverted(self):
            return ReciprocalScale.ReciprocalTransform()

    def get_transform(self):
        return self.ReciprocalTransform()

mscale.register_scale(ReciprocalScale)

ax2 = ax1.twiny()
ax2.set_xscale('reciprocal')
ax2.set_xticks(np.arange(pdm_freq[0],pdm_freq[-1])**(-1))
ax2.minorticks_on()
ax2.set_xlim((1./pdm_freq[0]), (1./pdm_freq[-1]))
ax2.set_xlabel('period [s]')

And this is what I get with it:

enter image description here

Asked By: sergiuspro

||

Answers:

Yay! I’ve figured it out by myself. 🙂 I was right in guessing that I had to change the set_default_locators_and_formatters() and the limit_range_for_scale() in the class ReciprocalScale(). So, in the end it’s really a merge of the two cases mentioned in my question.

This is the new code excerpt:

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.scale as mscale
import matplotlib.transforms as mtransforms

class ReciprocalScale(mscale.ScaleBase):
    """
    ScaleBase class for generating reciprocal scale.
    """

    name = 'reciprocal'

    def __init__(self, axis, **kwargs):
        mscale.ScaleBase.__init__(self)

    def set_default_locators_and_formatters(self, axis):
        class ReciprocalLocator(ticker.Locator):
            def __init__(self, numticks = 5):
                self.numticks = numticks
            def __call__(self):
                vmin, vmax = self.axis.get_view_interval()
                ticklocs = np.reciprocal(np.linspace(1/vmax, 1/vmin, self.numticks))
                return self.raise_if_exceeds(ticklocs)
        axis.set_major_locator(ReciprocalLocator(numticks=12))

    class ReciprocalTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True

        def transform_non_affine(self, a): 
            return np.array(a)**(-1)

        def inverted(self):
            return ReciprocalScale.InvertedReciprocalTransform()

    class InvertedReciprocalTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True

        def transform(self, a):
            return np.array(a)**(-1)

        def inverted(self):
            return ReciprocalScale.ReciprocalTransform()

    def get_transform(self):
        return self.ReciprocalTransform()

mscale.register_scale(ReciprocalScale)

ax2 = ax1.twiny()
ax2.set_xscale('reciprocal')
ax2.set_xlim((1./pdm_freq[-1]), (1./pdm_freq[0]))
ax2.set_xlabel('period [s]')

P.S. There is a minor issue, though. I get a RuntimeWarning: divide by zero encountered in reciprocal for return np.array(a)**(-1) which is correct, but I will mute it, because in my script I will never have 0 Hz frequency in the input data.

And this is what I get with the new code:

enter image description here

Answered By: sergiuspro

Thank you sergiuspro for the answer, it indeed works.

One issue that could occur however, is that the axis flips when the user is panning or zooming in interactive mode.

This has to do with the order being swapped around when reciprocal, resulting in xmin>xmax.

To solve this, one could add a callback to the axis that prevents this from happening.

One would add something to the end of the code like this:

def limit_range(evt):
    xlim = evt.get_xlim()
    xmin, xmax = xlim
    if xmin > xmax:
        evt.set_xlim(xmax, xmin)
ax.callbacks.connect('xlim_changed', limit_range)
Answered By: Maurits Houck
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.