How do I register the reciprocal (1/x) scale in Matplotlib?
Question:
Currently, I get this plot from my Python script:
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:
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:
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)
Currently, I get this plot from my Python script:
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:
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:
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)