I have to plot a 3d function which has meaningless negative values (they should not appear in the plot). The function which has to be plot is like:
def constraint_function(x, y):
return min(
(1800 - 0.3 * x - 0.5 * y) / 0.4,
(500 - 0.1 * x - 0.08 * y) / 0.12,
(200 - 0.06 * x - 0.04 * y) / 0.05
)
I'm calculating the function the following way:
xs = np.linspace(0, 3600, 1000)
ys = np.linspace(0, 3600, 1000)
zs = np.empty(shape=(1000, 1000))
for ix, x in enumerate(xs):
for iy, y in enumerate(ys):
zs[ix][iy] = constraint_function(x, y)
xs, ys = np.meshgrid(xs, ys)
The function has valid values mostly in the square [0, 3600]x[0, 3600]. The first approach I had is setting the axis limits to fit my needs:
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.azim = 20
ax.set_xlim(0, 3500)
ax.set_ylim(0, 3500)
ax.set_zlim(0, 4500)
ax.plot_surface(xs, ys, zs)
plt.show()
Which results in the following plot:
It just ignored the limits and did plot it anyway. The second approach was defining the negative values as np.nan changing the function to be as:
def constraint_function(x, y):
temp = min(
(1800 - 0.3 * x - 0.5 * y) / 0.4,
(500 - 0.1 * x - 0.08 * y) / 0.12,
(200 - 0.06 * x - 0.04 * y) / 0.05
)
return temp if temp >= 0 else np.nan
and setting the alpha of invalid values to zero:
plt.cm.jet.set_bad(alpha=0.0)
ax.azim = 20
ax.set_xlim(0, 3500)
ax.set_ylim(0, 3500)
ax.set_zlim(0, 4500)
ax.plot_surface(xs, ys, zs)
plt.show()
It leaves me with saw-like borders which is also something I don't want to have. Is there a way to get rid of these edges and getting a smooth line when the plot is turning negative?
First, your z-value array axes are reversed; it should be zs[iy][ix] not zs[ix][iy]. Because of this your plot is flipped left-for-right.
Second, building your z array by iterating in Python is much slower; you should instead delegate to numpy, like so:
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# create axis sample
xs = np.linspace(0, 3600, 1000)
ys = np.linspace(0, 3600, 1000)
# create mesh samples
xxs, yys = np.meshgrid(xs, ys)
# create data
zzs = np.min([
((1800 - 0.30 * xxs - 0.50 * yys) / 0.40),
(( 500 - 0.10 * xxs - 0.08 * yys) / 0.12),
(( 200 - 0.06 * xxs - 0.04 * yys) / 0.05)
], axis=0)
# clip data which is below 0.0
zzs[zzs < 0.] = np.NaN
NumPy vectorized operations are many times faster.
Third, there is nothing particularly wrong with your code except the sampling resolution is too low; set it higher,
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.azim = 20
ax.set_xlim(0, 3500)
ax.set_ylim(0, 3500)
ax.set_zlim(0, 4500)
ax.plot_surface(xxs, yys, zzs, rcount=200, ccount=200)
plt.show()
produces
Technically, you can skew the grid, such that the points of the grid, which would cause a zick-zack pattern are shifted such, that they lie on a line.
This is shown below.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
x=np.linspace(-5,5,6)
X,Y = np.meshgrid(x,x)
Z = X+Y
X[Z==-2] = X[Z==-2]+1
Y[Z==-2] = Y[Z==-2]+1
Z[Z==-2] = 0
Z[Z<0] = np.nan
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.set_zlim(0, 12)
ax.plot_surface(X, Y, Z)
plt.show()
The problem would now be to generalize this approach for arbitrary surfaces. It's sure possible but needs a bit of work.
Related
I would like to make surface plot of a function which is discontinuous at certain values in parameter space. It is near these discontinuities that the plot's coloring becomes incorrect, as shown in the picture below. How can I fix this?
My code is given below:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np
def phase(mu_a, mu_b, t, gamma):
theta = 0.5*np.arctan2(2*gamma, mu_b-mu_a)
epsilon = 2*gamma**2/np.sqrt((mu_a-mu_b)**2+4*gamma**2)
y1 = np.arccos(0.5/t*(-mu_a*np.sin(theta)**2 -mu_b*np.cos(theta)**2 - epsilon))
y2 = np.arccos(0.5/t*(-mu_a*np.cos(theta)**2 -mu_b*np.sin(theta)**2 + epsilon))
return y1+y2
fig = plt.figure()
ax = fig.gca(projection='3d')
# Make data.
X = np.arange(-2.5, 2.5, 0.01)
Y = np.arange(-2.5, 2.5, 0.01)
X, Y = np.meshgrid(X, Y)
Z = phase(X, Y, 1, 0.6)
# Plot the surface.
surf = ax.plot_surface(X, Y, Z, cmap=cm.coolwarm, linewidth=0, antialiased=False)
surf.set_clim(1, 5)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
An idea is to make all the arrays 1D, filter out the NaN values and then call ax.plot_trisurf:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np
def phase(mu_a, mu_b, t, gamma):
theta = 0.5 * np.arctan2(2 * gamma, mu_b - mu_a)
epsilon = 2 * gamma ** 2 / np.sqrt((mu_a - mu_b) ** 2 + 4 * gamma ** 2)
with np.errstate(divide='ignore', invalid='ignore'):
y1 = np.arccos(0.5 / t * (-mu_a * np.sin(theta) ** 2 - mu_b * np.cos(theta) ** 2 - epsilon))
y2 = np.arccos(0.5 / t * (-mu_a * np.cos(theta) ** 2 - mu_b * np.sin(theta) ** 2 + epsilon))
return y1 + y2
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
# Make data.
X = np.linspace(-2.5, 2.5, 200)
Y = np.linspace(-2.5, 2.5, 200)
X, Y = np.meshgrid(X, Y)
X = X.ravel() # make the array 1D
Y = Y.ravel()
Z = phase(X, Y, 1, 0.6)
mask = ~np.isnan(Z) # select the indices of the valid values
# Plot the surface.
surf = ax.plot_trisurf(X[mask], Y[mask], Z[mask], cmap=cm.coolwarm, linewidth=0, antialiased=False)
surf.set_clim(1, 5)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
Some remarks:
plot_trisurf will join the XY-values via triangles; this only works well if the domain is convex
to make things draw quicker, less points could be used (the original used 500x500 points, the code here reduces that to 200x200
calling fig.gca(projection='3d') has been deprecated; instead, you could call fig.add_subplot(projection='3d')
the warnings for dividing by zero or using arccos out of range can be temporarily suppressed; that way the warning will still be visible for situations when such isn't expected behavior
I have been trying to get a plot of vector lines going using the matplotlib library and I keep getting something like this:
Not sure what is happening since the code I'm running seems to follow the syntax for how to make a basic quiver plot. I've tried messing with the array type to see if that's the issue but no luck. Some points on the plot just don't seem to be getting any vector data.
import matplotlib.pyplot as plt
import numpy as np
X = np.arange(-2,2,.1)
Y = np.arange(-2,2,.1)
x,y = np.meshgrid(X,Y)
m1 =1
m2 =2
x1 =4/3
x2 =2/3
omega = 3/8
u = -(m1/(abs(x-x1))**3)*(x-x1)-(m2/(abs(x-x2))**3)*(x-x2)+ x*omega
v = -(m1/(abs(y))**3)*(y)-(m2/(abs(y))**3)*(y)+ y*omega
fig, ax = plt.subplots()
ax.quiver(x,y,u,v)
plt.show()
A nice way, I find, to have a look at your data is to normalise the vector field and colour it by intensity. You can always mask glyphs for which the intensity is too low by using a Numpy MaskedArray. Have a look below.
import matplotlib.colors as cl
import matplotlib.pyplot as plt
import matplotlib.ticker as tck
import numpy as np
x, y = np.meshgrid(np.linspace(-2, 2, 41), np.linspace(-2, 2, 41))
m1, m2, x1, x2, omega = (1, 2, 4 / 3, 2 / 3, 3 / 8)
u = -(m1 / abs(x - x1) ** 3 * (x - x1) - m2 / abs(x - x2) ** 3 * (x - x2)
+ x * omega)
v = y * (omega - (m1 + m2) / abs(y) ** 3)
fig, (ax, bx) = plt.subplots(ncols=2, figsize=(20, 10))
ax.quiver(x, y, u, v, antialiased=True, scale=1e4, width=6e-3, headwidth=3,
headlength=4, headaxislength=3.5, pivot='tail',
edgecolors='xkcd:white', linewidths=1)
ax.set_aspect('equal')
w = np.sqrt(u ** 2 + v ** 2)
quiv = bx.quiver(x, y, u / w, v / w, w, antialiased=True, scale=3e1,
width=6e-3, headwidth=3, headlength=4, headaxislength=3.5,
pivot='tail', edgecolors='xkcd:white', linewidths=1,
norm=cl.LogNorm(vmin=1e-1, vmax=1e3))
bx.set_aspect('equal')
fig.colorbar(quiv, cax=fig.add_axes([0.93, 0.1, 0.02, 0.8]),
extend='both', ticks=tck.LogLocator(),
format=tck.LogFormatterSciNotation())
Some of the y values are close to 0 so that you get crazily large v values. I would check the equation because the plot is actually correct (the arrows are infinitely large when y ~= 0).
I have been trying to make a plot with some evenly spaced tick in my colorbar, but so far my results always give me a colorbar with the distance between the ticks proportional to their values as shown in the image below:
import numpy as np
import matplotlib
import matplotlib as plt
T= [0.01, 0.02, 0.03, 0.04] #values for the colourbar to use in equation in for loop
x=np.linspace[0, 8, 100]
e=1/(np.exp(x)+1) #factor used in equation dependent on the x-axis values
a=6.4*10**(-9)
b= 1.51 # constants for the equation
pof6= [number **6 for number in T]
norm = matplotlib.colors.Normalize(vmin=np.min(pof6), vmax=np.max(pof6)) #colourbar max and min values
c_m = matplotlib.cm.cool
s_m = matplotlib.cm.ScalarMappable(cmap='jet', norm=norm)
s_m.set_array([])
#below is the for loop that uses one value of T at a time, represented as t in the equation
for t in pof6:
plt.plot(x, b*x/(((a*t*x**2/(m**2))+1)**2)*e, color=s_m.to_rgba(t))
func = lambda x,pos: "{:g}".format(x)
fmt = matplotlib.ticker.FuncFormatter(func)
c_bar=plt.colorbar(s_m, format=fmt, ticks=[0.01**6,0.02* 0.03**6, 0.04**6])
plt.legend()
plt.xlabel('y=E/T')
plt.ylabel('$f_{ν_s}$')
c_bar.set_label(r'T(K)')
plt.show()
I have attempted applying some of the solutions suggested here n=on the website, like Spread custom tick labels evenly over colorbar but haven't been successful at that.
You're using a linear norm, where the pof values are very close to each other. It helps to use a LogNorm. The tick formatter can be adapted to show the values in their **6 format.
The code below shifts the four functions a bit, because with the code from the example all plots seem to coincide. At least when I use something like m=2 (m is not defined in the code).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors as mcolors
from matplotlib import ticker as mticker
T = [0.01, 0.02, 0.03, 0.04] # values for the colourbar to use in equation in for loop
x = np.linspace(0, 8, 100)
e = 1 / (np.exp(x) + 1) # factor used in equation dependent on the x-axis values
a = 6.4 * 10 ** (-9)
b = 1.51 # constants for the equation
pof6 = [number ** 6 for number in T]
norm = mcolors.LogNorm(vmin=np.min(pof6), vmax=np.max(pof6)) # colourbar max and min values
s_m = plt.cm.ScalarMappable(cmap='jet', norm=norm)
s_m.set_array([])
m = 2
for t in pof6:
plt.plot(x, b * x / (((a * t * x ** 2 / (m ** 2)) + 1) ** 2) * e + 10*t**(1/6), color=s_m.to_rgba(t))
func = lambda x, pos: "{:g}**6".format(x**(1/6) )
fmt = mticker.FuncFormatter(func)
c_bar = plt.colorbar(s_m, format=fmt, ticks=pof6)
c_bar.set_label(r'T(K)')
# plt.legend() # there are no labels set, so a default legend can't be created
plt.xlabel('y=E/T')
plt.ylabel('$f_{ν_s}$')
plt.show()
If you want a legend, you need to put a label to each curve, for example:
for t in pof6:
plt.plot(x, b * x / (((a * t * x ** 2 / (m ** 2)) + 1) ** 2) * e, color=s_m.to_rgba(t),
label=f'$t = {t**(1/6):g}^6$')
plt.legend()
I'd like to create an inset within my fig which is a zoom in on part of my polar plot.
I've tried various different methods but can't seem to crack the correct way to do using matplotlib. My code to create the plot (from my pandas dataframe) is below. I've also included the plot that it produces.
def plot_polar_chart_new(n, start, df, sales, title):
HSV_tuples = [(x * 1.0 / n, 0.5, 0.5) for x in range(n)]
RGB_tuples = map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
RGB_normalised = [tuple(n / max(t) for n in t) for t in RGB_tuples]
figsize=(15, 15)
fig = mpl.pyplot.figure(figsize=figsize)
ax = fig.add_subplot(1,1,1, polar=True)
start = 0
prev_count = 0
for i, salesperson in enumerate(sales):
count, division = (df[salesperson], df.index)
ax.bar((division - start) * 2 * np.pi / N, height=count, width=2 * np.pi / N, color=RGB_normalised[i], bottom=prev_count, label=salesperson)
prev_count += count
ax.set_xticks(np.linspace(0, 2 * np.pi, N, endpoint=False))
ax.set_xticklabels(range(start, N + start),fontsize=20)
ax.yaxis.set_tick_params(labelsize=20)
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.set_title(title, y=1.1, fontsize=20)
ax.legend(bbox_to_anchor=(0.9, 1.1), loc=2)
mpl.pyplot.show()
I'd like to create a plot inset which zooms in on part of the plot between 17 and 02.
Please help!
Thanks
From a set of angle (theta) and radius (r) I drew a scatter plot using matplotlib:
fig = plt.figure()
ax = plt.subplot(111, polar=True)
ax.scatter(theta, r, color='None', edgecolor='red')
ax.set_rmax(1)
plt.savefig("polar.eps",bbox_inches='tight')
Which gave me this figure
I now want to draw the density contour map on top of that, so I tried:
fig = plt.figure()
ax = plt.subplot(111, polar=True)
H, theta_edges, r_edges = np.histogram2d(theta, r)
cax = ax.contourf(theta_edges[:-1], r_edges[:-1], H, 10, cmap=plt.cm.Spectral)
ax.set_rmax(1)
plt.savefig("polar.eps",bbox_inches='tight')
Which gave me the following results that is obviously not what I wanted to do.
What am I doing wrong ?
I think that the solution to your problem is to define the bins arrays for your histogram (for instance a linspaced array between 0 and 2pi for theta and between 0 and 1 for r). This can be done with the bins or range arguments of function numpy.histogram
I you do so, make sure that the theta values are all between 0 and 2pi by plotting theta % (2 * pi) instead of theta.
Finally, you may choose to plot the middle of the bin edges instead of the left side of the bins as done in your example (use 0.5 * (r_edges[1:] + r_edges[:-1]) instead of r_edges[:-1])
below is a suggestion of code
import matplotlib.pyplot as plt
import numpy as np
#create the data
r1 = .2 + .2 * np.random.randn(200)
theta1 = 0. + np.pi / 7. * np.random.randn(len(r1))
r2 = .8 + .2 * np.random.randn(300)
theta2 = .75 * np.pi + np.pi / 7. * np.random.randn(len(r2))
r = np.concatenate((r1, r2))
theta = np.concatenate((theta1, theta2))
fig = plt.figure()
ax = plt.subplot(111, polar=True)
#define the bin spaces
r_bins = np.linspace(0., 1., 12)
N_theta = 36
d_theta = 2. * np.pi / (N_theta + 1.)
theta_bins = np.linspace(-d_theta / 2., 2. * np.pi + d_theta / 2., N_theta)
H, theta_edges, r_edges = np.histogram2d(theta % (2. * np.pi), r, bins = (theta_bins, r_bins))
#plot data in the middle of the bins
r_mid = .5 * (r_edges[:-1] + r_edges[1:])
theta_mid = .5 * (theta_edges[:-1] + theta_edges[1:])
cax = ax.contourf(theta_mid, r_mid, H.T, 10, cmap=plt.cm.Spectral)
ax.scatter(theta, r, color='k', marker='+')
ax.set_rmax(1)
plt.show()
which should result as