I'm trying to scale the x axis of a plot with math.log(1+x) instead of the usual 'log' scale option, and I've looked over some of the custom scaling examples but I can't get mine to work! Here's my MWE:
import matplotlib.pyplot as plt
import numpy as np
import math
from matplotlib.ticker import FormatStrFormatter
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform_non_affine(self, a):
return math.log(1+a)
def inverted(self):
return CustomScale.InvertedCustomTransform(self.thresh)
class InvertedCustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform_non_affine(self, a):
return math.log(1+a)
def inverted(self):
return CustomScale.CustomTransform(self.thresh)
# Now that the Scale class has been defined, it must be registered so
# that ``matplotlib`` can find it.
mscale.register_scale(CustomScale)
z = [0,0.1,0.3,0.9,1,2,5]
thick = [20,40,20,60,37,32,21]
fig = plt.figure(figsize=(8,5))
ax1 = fig.add_subplot(111)
ax1.plot(z, thick, marker='o', linewidth=2, c='k')
plt.xlabel(r'$\rm{redshift}$', size=16)
plt.ylabel(r'$\rm{thickness\ (kpc)}$', size=16)
plt.gca().set_xscale('custom')
plt.show()
The scale consists of two Transform classes, each of which needs to provide a transform_non_affine method. One class needs to transform from data to display coordinates, which would be log(a+1), the other is the inverse and needs to transform from display to data coordinates, which would in this case be exp(a)-1.
Those methods need to handle numpy arrays, so they should use the respective numpy functions instead of those from the math package.
class CustomTransform(mtransforms.Transform):
....
def transform_non_affine(self, a):
return np.log(1+a)
class InvertedCustomTransform(mtransforms.Transform):
....
def transform_non_affine(self, a):
return np.exp(a)-1
There's no need to define classes yourself even the answer from #ImportanceOfBeingErnest does work.
You can use either FunctionScale or FunctionScaleLog to do this in one line. Take the FunctionScaleLog as example:
plt.gca().set_xscale("functionlog", functions=[lambda x: x + 1, lambda x: x - 1])
And with your full code:
import matplotlib.pyplot as plt
import numpy as np
z = [0, 0.1, 0.3, 0.9, 1, 2, 5]
thick = [20, 40, 20, 60, 37, 32, 21]
fig = plt.figure(figsize=(8, 5))
ax1 = fig.add_subplot(111)
ax1.plot(z, thick, marker="o", linewidth=2, c="k")
plt.xlabel(r"$\rm{redshift}$", size=16)
plt.ylabel(r"$\rm{thickness\ (kpc)}$", size=16)
# Below is my code
plt.gca().set_xscale("functionlog", functions=[lambda x: x + 1, lambda x: x - 1])
plt.gca().set_xticks(np.arange(0, 6))
plt.gca().set_xticklabels(np.arange(0, 6))
And the result is:
Related
I'm unsure the best way to do the following. That is, I'm not sure if I should have a parent class UniSamplingStrategy and child classes UniformSampling, and RandomSampling. Or should I just have UniSamplingStrategy and have the types of samplings as methods? For example, this is what I did:
import numpy as np
## make a base class w/ child classes instead?
class UniSamplingStrategy():
def __init__(self,
left=0,
right=0,
num_samples=0,
cluster_center=None,
defined_array=[0]
):
self._left = left
self._right = right
self._num_samples = num_samples
self._cluster_center = cluster_center
self._defined_array = defined_array
# uniform sampling
def uniform_sampling(self):
return np.linspace(start=self._left,
stop=self._right,
num=self._num_samples,
endpoint=True,
dtype=np.float32)
# random spacing
def clustered_sampling(self):
return np.random.normal(loc=self._clust_center,
scale=(self._right - self._left)/4,
size=self._num_samples)
What I want to do with this class (or perhaps classes, if I need to rewrite for good python) is pass a sampling strategy to my data_generation method.
def data_generation(noise_scale,
sampling_strategy,
test_func,
noise_type
):
x_samples = sampling_strategy
y_samples = test_func(x=x_samples)
if noise_type is not None:
_, y_samples_noise = noise_type(x=x_samples, scale=noise_scale)
y_samples = y_samples + y_samples_noise
return x_samples, y_samples
def test_func(x):
return (np.cos(x))**2/((x/6)**2+1)
def hmskd_noise(x, scale):
scales = scale
return scales, np.random.normal(scale=scale, size=x.shape[0])
So that ideally, I could try different test functions, noise, and sampling schemes. Where I could write function calls like:
x_true, y_true = data_generation(sampling_strategy=uniform_sampling(left=0, right=10, num_samples=1000)
test_func = test_func,
noise_type=None,
noise_scale = 0)
x_obs, y_obs = data_generation(sampling_strategy=clustered_sampling(clustered_center=5, left=0, right=10, num_samples = 20),
test_func = test_func,
noise_type=hmskd_noise,
noise_scale=0.2)
Essentially, I'm interested in the best way to pass a sampling strategy to data_generation when each method can have different parameters to pass (e.g., see uniform_sampling and clustered_sampling parameters).
Thanks for your time :)
For example, you can have a set of classes with __call__ method. Like
class UniformSampling:
def __init__(self,
left=0,
right=0,
num_samples=0,
cluster_center=None,
defined_array=[0]
):
self._left = left
self._right = right
self._num_samples = num_samples
self._cluster_center = cluster_center
self._defined_array = defined_array
def __call__(self, arg1, arg2):
return np.linspace(start=self._left,
stop=self._right,
num=self._num_samples,
endpoint=True,
dtype=np.float32)
Then you can pass instantiated object to data_generation as
x_true, y_true = data_generation(sampling_strategy=UniformSampling(left=0, right=10, num_samples=1000),
test_func = test_func,
noise_type=None,
noise_scale = 0)
From the following objected oriented class for polynomial, we can get a symbolic expression of the polynomial.
#Classes and Object Oriented Programming
import numpy as np
import matplotlib.pyplot as plt
class Polynomial(object):
def __init__(self, roots, leading_term):
self.roots = roots
self.leading_term = leading_term
self.order = len(roots)
def __repr__(self):
string = str(self.leading_term)
for root in self.roots:
if root == 0:
string = string + "x"
elif root > 0:
string = string + "(x - {})".format(root)
else:
string = string + "(x + {})".format(-root)
return string
def __mul__(self, other):
roots = self.roots + other.roots
leading_term = self.leading_term * other.leading_term
return Polynomial(roots, leading_term)
def explain_to(self, caller):
print("Hello, {}. {}.".format(caller,self.explanation))
print("My roots are {}.".format(self.roots))
# Creating a symbolic expression of the polynomial
>>> p = Polynomial((1,2,-3),2)
>>> print(p)
2(x - 1)(x - 2)(x + 3)
>>> q = Polynomial((1,1,0,-2), -1)
>>> print(q)
-1(x - 1)(x - 1)x(x + 2)
For plotting purpose, I am willing to avoid the following approach and directly want to covert the symbolic expressipon into a collable expression in the polynomial class itself.
# Plotting the polynomials
def p(x):
return 2*(x-1)*(x-2)*(x+3)
def q(x):
return -1*(x-1)*(x-1)*x*(x+2)
x = np.linspace(-5,5,100)
plt.plot(x, p(x),'r--')
plt.plot(x, q(x), 'g--')
If I get you right, you want something like this using __call__.
Here is how to use __call__ in your class:
import numpy as np
import matplotlib.pyplot as plt
class Polynomial(object):
def __init__(self, roots, leading_term):
self.roots = roots
self.leading_term = leading_term
self.order = len(roots)
def __repr__(self):
string = str(self.leading_term)
for root in self.roots:
if root == 0:
string = string + "x"
elif root > 0:
string = string + "(x - {})".format(root)
else:
string = string + "(x + {})".format(-root)
return string
def __call__(self,x):
res = 1
res *= self.leading_term
for root in self.roots:
res *= (x - root)
return res
def __mul__(self, other):
roots = self.roots + other.roots
leading_term = self.leading_term * other.leading_term
return Polynomial(roots, leading_term)
def explain_to(self, caller):
print("Hello, {}. {}.".format(caller,self.explanation))
print("My roots are {}.".format(self.roots))
It works like this:
>>> p = Polynomial((1,2,-3),2)
>>> print(p)
2(x - 1)(x - 2)(x + 3)
>>> print(p(0), p(1), p(2), p(3) ,p(4))
12 0 0 24 84
>>> x = np.linspace(-4, 4, 100)
>>> fig = plt.figure()
>>> plt.plot(x, p(x), '-')
Here is a solution that relies on sympy. Note that instead of using lambdify, coefficients are extracted and passed into np.poly1d. As a result, the callable function is vectorized and should be fairly efficient.
import numpy as np
from sympy import Poly, sympify
from matplotlib import pyplot as plt
poly_string = "2*x**3 + 4"
coeffs = Poly(sympify(poly_string)).all_coeffs()
p_callable = np.poly1d(np.array(coeffs).astype(np.float))
xs = np.linspace(-10,10,100)
ys = p_callable(xs)
plt.plot(xs, ys)
You can do that purely in numpy (np)
p = 2*np.poly([1,2,-3])
q = -1*np.poly([1,1,0,-2])
x = np.linspace(-5,5,100)
plt.plot(x, np.polyval(p,x),'r--')
plt.plot(x, np.polyval(q,x), 'g--')
plt.grid(); plt.legend("pq"); plt.show()
UPDATED:
I would like to plot real time y values generated randomly from Random_Generation_List.py. I have imported the python file and have them in the same folder. The points are only being printed and show only a vertical line on the graph. How do I fix this to make the points plot onto the graph in real time? Like a point every 0.001?
Random_Generation_List:
import random
import threading
def main():
for count in range(12000):
y = random.randint(0,1000)
print(y)
def getvalues():
return [random.randint(0,1000) for count in range(12000)]
def coordinate():
threading.Timer(0.0001, coordinate).start ()
coordinate()
main()
Real_Time_Graph
import time
from collections import deque
from matplotlib import pyplot as plt
from matplotlib import style
import Random_Generation_List
start = time.time()
class RealtimePlot:
def __init__(self, axes, max_entries = 100):
self.axis_x = deque(maxlen=max_entries)
self.axis_y = deque(maxlen=max_entries)
self.axes = axes
self.max_entries = max_entries
self.lineplot, = axes.plot([], [], "g-")
self.axes.set_autoscaley_on(True)
def add(self, x, y):
self.axis_x.append(x)
self.axis_y.append(y)
self.lineplot.set_data(self.axis_x, self.axis_y)
self.axes.set_xlim(self.axis_x[0], self.axis_x[-1] + 1e-15)
self.axes.relim(); self.axes.autoscale_view() # rescale the y-axis
def animate(self, figure, callback, interval = 50):
def wrapper(frame_index):
self.add(*callback(frame_index))
self.axes.relim(); self.axes.autoscale_view() # rescale the y-axis
return self.lineplot
def main():
style.use('dark_background')
fig, axes = plt.subplots()
display = RealtimePlot(axes)
axes.set_xlabel("Seconds")
axes.set_ylabel("Amplitude")
values = Random_Generation_List.getvalues()
print(values)
while True:
display.add(time.time() - start, values)
plt.pause(0.001)
display.animate(fig, lambda frame_index: (time.time() - start, values))
plt.show()
if __name__ == "__main__": main()
Error Message:
raise RuntimeError('xdata and ydata must be the same length')
RuntimeError: xdata and ydata must be the same length
I see that set_xscale accepts a base parameter, but I also want to scale with a factor; i.e. if the base is 4 and the factor is 10, then:
40, 160, 640, ...
Also, the documentation says that the sub-grid values represented by subsx should be integers, but I will want floating-point values.
What is the cleanest way to do this?
I'm not aware of any built-in method to apply a scaling factor after the exponent, but you could create a custom tick locator and formatter by subclassing matplotlib.ticker.LogLocator and matplotlib.ticker.LogFormatter.
Here's a fairly quick-and-dirty hack that does what you're looking for:
from matplotlib import pyplot as plt
from matplotlib.ticker import LogLocator, LogFormatter, ScalarFormatter, \
is_close_to_int, nearest_long
import numpy as np
import math
class ScaledLogLocator(LogLocator):
def __init__(self, *args, scale=10.0, **kwargs):
self._scale = scale
LogLocator.__init__(self, *args, **kwargs)
def view_limits(self, vmin, vmax):
s = self._scale
vmin, vmax = LogLocator.view_limits(self, vmin / s, vmax / s)
return s * vmin, s * vmax
def tick_values(self, vmin, vmax):
s = self._scale
locs = LogLocator.tick_values(self, vmin / s, vmax / s)
return s * locs
class ScaledLogFormatter(LogFormatter):
def __init__(self, *args, scale=10.0, **kwargs):
self._scale = scale
LogFormatter.__init__(self, *args, **kwargs)
def __call__(self, x, pos=None):
b = self._base
s = self._scale
# only label the decades
if x == 0:
return '$\mathdefault{0}$'
fx = math.log(abs(x / s)) / math.log(b)
is_decade = is_close_to_int(fx)
sign_string = '-' if x < 0 else ''
# use string formatting of the base if it is not an integer
if b % 1 == 0.0:
base = '%d' % b
else:
base = '%s' % b
scale = '%d' % s
if not is_decade and self.labelOnlyBase:
return ''
elif not is_decade:
return ('$\mathdefault{%s%s\times%s^{%.2f}}$'
% (sign_string, scale, base, fx))
else:
return (r'$%s%s\times%s^{%d}$'
% (sign_string, scale, base, nearest_long(fx)))
For example:
fig, ax = plt.subplots(1, 1)
x = np.arange(1000)
y = np.random.randn(1000)
ax.plot(x, y)
ax.set_xscale('log')
subs = np.linspace(0, 1, 10)
majloc = ScaledLogLocator(scale=10, base=4)
minloc = ScaledLogLocator(scale=10, base=4, subs=subs)
fmt = ScaledLogFormatter(scale=10, base=4)
ax.xaxis.set_major_locator(majloc)
ax.xaxis.set_minor_locator(minloc)
ax.xaxis.set_major_formatter(fmt)
ax.grid(True)
# show the same tick locations with non-exponential labels
ax2 = ax.twiny()
ax2.set_xscale('log')
ax2.set_xlim(*ax.get_xlim())
fmt2 = ScalarFormatter()
ax2.xaxis.set_major_locator(majloc)
ax2.xaxis.set_minor_locator(minloc)
ax2.xaxis.set_major_formatter(fmt2)
I'm trying to do a blank Stüve diagram.
My code is:
R=287.04 #Jkg^-1K^-1
cp=1005 #Jkg^-1K^-1
p0=1000 #hPa
L=2.5*10**6 #J kg^-1
temp_celsius = np.array(range(-80,41))
temp_kelvin=temp_celsius+273.15
pressure_hPa=np.array(range(1050,90,-10))
#make a grid of the data
tempdata,pressdata=np.meshgrid(temp_kelvin,pressure_hPa*100.)
#Initialise the arrays
pot_temp_kelvin=np.zeros(tempdata.shape)
es=pot_temp_kelvin
ms=es
pseudo_pot_temp_kelvin=es
#Get the potential temperature
pot_temp_kelvin=tempdata*(p0*100/pressdata)**(R/cp)
#Get the saturation mix ratio
#first the saturation vapor pressure after Magnus
#Definition of constants for the Magnus-formula
c1=17.62
c2=243.12
es=6.112*np.exp((17.62*(tempdata-273.15))/(243.12+tempdata-273.15)) #hPa
#Now I'm calculation the saturation mixing ratio
ms=622*(es/(pressdata/100-es)) #g/kg
#At least I need the pseudo-adiabatic potential temperature
pseudo_pot_temp_kelvin=pot_temp_kelvin*np.exp(L*ms/1000./cp/tempdata)
#define the levels which should be plotted in the figure
levels_theta=np.array(range(200,405,5))
levels_ms=np.array([0.1,0.2,0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 15.0, 20.0, 25.0, 30.0])
levels_theta_e=np.array(range(220,410,10))
#The plot
fig = plt.figure(figsize=(15,15))
theta=plt.contour(temp_celsius,pressure_hPa,pot_temp_kelvin,levels_theta,colors='blue')
plt.clabel(theta,levels_theta[0::2], fontsize=10, fmt='%1.0f')
sat_mix_ratio=plt.contour(temp_celsius,pressure_hPa,ms,levels_ms,colors='green')
plt.clabel(sat_mix_ratio,fontsize=10,fmt='%1.1f')
theta_e=plt.contour(temp_celsius,pressure_hPa,pseudo_pot_temp_kelvin,levels_theta_e,colors='red')
plt.clabel(theta_e,levels_theta_e[1::2],fontsize=10,fmt='%1.0f')
plt.xlabel('Temperature [$^\circ$C]')
plt.ylabel('Pressure [hPa]')
plt.xticks(range(-80,45,5))
plt.xlim((-80,50))
plt.yticks(range(1050,50,-50))
plt.gca().invert_yaxis()
plt.grid(color='black',linestyle='-')
plt.show()
Everything works well but the real Stüve diagram should look like Stüve diagram with sounding
As you can see, the y-axis has a specific scale: p**(R/cp).... (=p**(287.04)/1005)
What do I have to do with my program so that my y-axis looks like the axis in the example?
You will have to define your own axis scale for this. My answer is based on this answer and the custom scale example.
This is the custom scaling class:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FormatStrFormatter
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform_non_affine(self, a):
return a**(R/cp)
def inverted(self):
return CustomScale.InvertedCustomTransform(self.thresh)
class InvertedCustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform_non_affine(self, a):
return a**(cp/R)
def inverted(self):
return CustomScale.CustomTransform(self.thresh)
# Now that the Scale class has been defined, it must be registered so
# that ``matplotlib`` can find it.
mscale.register_scale(CustomScale)
Now, you can use the following option to provide your custom scale to your y=axis:
plt.gca().set_yscale('custom')
The following plot compares the custom scale to a log-scale (plt.gca().set_yscale('log')) and the default without scale: