Using numpy/scipy to calculate iso-surface from 3D array - python

I have a 3D numpy array that contains the values of a given function. I want to calculate a 2D iso-surface, or a set of iso-surfaces that represent certain values of this function.
In this particular case, each 1D column (column = myarray[i, j, :]) of the 3D array can be treated independently. So what I would like to know are the last index positions (2D array) where the function is equal to a certain value, say myvalue.
Some (slow) code to exemplify:
# myarray = 3D ndarray
import numpy as np
from scipy import interpolate
result = np.zeros(nx, ny)
z_values = np.arange(nz)
for i in range(nx):
for j in range(ny):
f = interpolate.interp1d(my_array[i, j], z_values)
result[i, j] = f(myvalue)
I know this can be sped up a bit with np.ndenumerate and other tricks, but was wondering if there is already a simpler way of doing this kind of iso-surface. I couldn't find anything in ndimage or other libraries. I know that mayavi2 and vtk have a lot of tools to deal with iso-surfaces, but my aim here is not visualisation -- I want to perform calculations on those iso-surface values, not display them. Plus, a lot of the iso-surface methods of vtk seem to involve polygons and the like, and what I need is just a 2D array of positions for each iso-surface value.

Using only numpy you can get a good solution using argsort, sort, take and the proper array manipulation. The function below uses a weighted average to compute the iso-surface:
def calc_iso_surface(my_array, my_value, zs, interp_order=6, power_parameter=0.5):
if interp_order < 1: interp_order = 1
from numpy import argsort, take, clip, zeros
dist = (my_array - my_value)**2
arg = argsort(dist,axis=2)
dist.sort(axis=2)
w_total = 0.
z = zeros(my_array.shape[:2], dtype=float)
for i in xrange(int(interp_order)):
zi = take(zs, arg[:,:,i])
valuei = dist[:,:,i]
wi = 1/valuei
clip(wi, 0, 1.e6, out=wi) # avoiding overflows
w_total += wi**power_parameter
z += zi*wi**power_parameter
z /= w_total
return z
This solution does not handle situations where there is more than one z corresponding to my_value. An application example to build the iso-surfaces below is given in the following code:
from numpy import meshgrid, sin, cos, pi, linspace
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
dx = 100; dy = 50; dz = 25
nx = 200; ny = 100; nz = 100
xs = linspace(0,dx,nx)
ys = linspace(0,dy,ny)
zs = linspace(0,dz,nz)
X,Y,Z = meshgrid( xs, ys, zs, dtype=float)
my_array = sin(0.3*pi+0.4*pi*X/dx)*sin(0.3*pi+0.4*pi*Y/dy)*(Z/dz)
fig = plt.figure()
ax = fig.gca(projection='3d')
z = calc_iso_surface( my_array, my_value=0.1, zs=zs, interp_order=6 )
ax.plot_surface(X[:,:,0], Y[:,:,0], z, cstride=4, rstride=4, color='g')
z = calc_iso_surface( my_array, my_value=0.2, zs=zs, interp_order=6 )
ax.plot_surface(X[:,:,0], Y[:,:,0], z, cstride=4, rstride=4, color='y')
z = calc_iso_surface( my_array, my_value=0.3, zs=zs, interp_order=6 )
ax.plot_surface(X[:,:,0], Y[:,:,0], z, cstride=4, rstride=4, color='b')
plt.ion()
plt.show()
You can also play with different interpolation functions. See below one example that takes the average of the two closest zs:
def calc_iso_surface_2(my_array, my_value, zs):
'''Takes the average of the two closest zs
'''
from numpy import argsort, take
dist = (my_array - my_value)**2
arg = argsort(dist,axis=2)
z0 = take(zs, arg[:,:,0])
z1 = take(zs, arg[:,:,1])
z = (z0+z1)/2
return z

Related

How to rotate a cylinder without causing a 'sheared' appearance

I have plotted a 'tear drop' shaped cylinder in matplotlib. To obtain the tear drop shape I plotted a normal cylinder from theta = 0 to theta = pi and an ellipse from theta = pi to theta = 2pi. However I am now trying to 'spin' the cylinder around it's axis which here is given conveniently by the z-axis.
I tried using the rotation matrix for rotating around the z-axis which Wikipedia gives as:
However when I try to rotate through -pi/3 radians, the cylinder becomes very disfigured.
Is there anyway to prevent this from happening?
Here is my code:
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from math import sin, cos, pi
import math
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
theta = np.linspace(0,2*pi, 1200)
Z = np.linspace(0,5,1000+600)
Z,theta = np.meshgrid(Z, theta)
X = []
Y = []
R = 0.003
#calculate the x and y values
for i in theta:
cnt = 0
tempX = []
tempY = []
for j in i:
#circle
if(i[0]<=pi):
tempX.append(R*cos(j))
tempY.append(R*sin(j))
cnt+=1
#ellipse
else:
tempX.append(R*cos(j))
tempY.append(0.006*sin(j))
X.append(tempX)
Y.append(tempY)
X1 = np.array(X)
Y1 = np.array(Y)
#rotate around the Z axis
a = -pi/3
for i in range(len(X)):
X1[i] = cos(a)*X1[i]-sin(a)*Y1[i]
Y1[i] = sin(a)*X1[i]+cos(a)*Y1[i]
#plot
ax.plot_surface(X1,Y1,Z,linewidth = 0, shade = True, alpha = 0.3)
ax.set_xlim(-0.01,0.01)
ax.set_ylim(-0.01, 0.01)
azimuth = 173
elevation = 52
ax.view_init(elevation, azimuth)
plt.show()
Your rotating is flawed: To calculate Y1[i] you need the old value of X1[i], but you already updated it. You can try something like
X1[i], Y1[i] = cos(a)*X1[i]-sin(a)*Y1[i], sin(a)*X1[i]+cos(a)*Y1[i]
if you want to make the matrix multiplication a bit more obvious (and fix the bug) you could also do the following (please doublecheck that the matrix is correct and that the multiplication is in the right order, I did not test this):
rotation_matrix = np.array([[cos(a), -sin(a)], [sin(a), cos(a)]])
x, y = zip(*[(x,y) # rotation_matrix for x,y in zip(x,y)])
the # is new in 3.5 and for numpy array it's defined to be the matrix multiplication. If you are on a version below 3.5 you can use np.dot.
The zip(*...) is necessary to get a pair of lists instead of a list of pairs. See also this answer

Python: is it possible to interpolate a matrix in a MATLAB style?

I have difficulty to interpolate a matrix/data frame in python.
Suppose we I have a matrix M = 3x4 and x = [1 3 5], y = [0.1 0.4 0.5 0.7]
This is my way to do interpolate and then plot in Matlab.
xq = 1:1:5;
yq = 0.1:0.1:1;
[xq,yq] = meshgrid(xq,yq);
zq = interp2(y,x,M,xq,yq);
figure
h=pcolor(xq,yq,zq)
set(h,'EdgeColor','none')
This is a possible way in Python
from scipy import interpolate
import numpy as np
def my_interp(X, Y, Z, x, y, spn=3):
xs,ys = map(np.array,(x,y))
z = np.zeros(xs.shape)
for i,(x,y) in enumerate(zip(xs,ys)):
# get the indices of the nearest x,y
xi = np.argmin(np.abs(X[0,:]-x))
yi = np.argmin(np.abs(Y[:,0]-y))
xlo = max(xi-spn, 0)
ylo = max(yi-spn, 0)
xhi = min(xi+spn, X[0,:].size)
yhi = min(yi+spn, Y[:,0].size)
# make slices of X,Y,Z that are only a few items wide
nX = X[xlo:xhi, ylo:yhi]
nY = Y[xlo:xhi, ylo:yhi]
nZ = Z[xlo:xhi, ylo:yhi]
intp = interpolate.interp2d(nX, nY, nZ)
z[i] = intp(x,y)[0]
return z
zq = my_interp(y, x, M, xq, yq)
As I noted in a comment, your code is 1:1 translatable to python using the necessary libraries. You need numpy for linspace/meshgrid, matplotlib.pyplot for pcolor(mesh), scipy.interpolate for griddata. I was going to say "interp2d is available but don't use it, but it turns out that you need extrapolation outside the convex hull of your input data, so griddata won't cut it. Here's a solution with interp2d, but take the results with a grain of salt:
import numpy as np
import scipy.interpolate as interp
import matplotlib.pyplot as plt
# input
xv = np.array([1, 3, 5])
yv = np.array([0.1, 0.4, 0.5, 0.7])
x,y = np.meshgrid(xv,yv)
M = np.random.rand(4,3)
xqv = np.arange(1,6)
yqv = np.arange(0.1,1.1,0.1)
xq,yq = np.meshgrid(xqv,yqv)
zqfun = interp.interp2d(x,y,M)
zq = zqfun(xqv,yqv)
plt.figure()
#h = plt.pcolor(xq,yq,zq)
h = plt.pcolormesh(xq,yq,zq) # <-- same thing but faster
Result(left) compared with your MATLAB original (right; after fixing the order x,y,M in interp2d):
You can see that the results differ at the sides, this is because MATLAB always throws away the last row and column of the data, while matplotlib doesn't.

Using meshgrid to convert X,Y,Z triplet to three 2D arrays for surface plot in matplotlib

I'm new to Python so please be patient. I appreciate any help!
What I have: three 1D lists (xr, yr, zr), one containing x-values, the other two y- and z-values
What I want to do: create a 3D contour plot in matplotlib
I realized that I need to convert the three 1D lists into three 2D lists, by using the meshgrid function.
Here's what I have so far:
xr = np.asarray(xr)
yr = np.asarray(yr)
zr = np.asarray(zr)
X, Y = np.meshgrid(xr,yr)
znew = np.array([zr for x,y in zip(np.ravel(X), np.ravel(Y))])
Z = znew.reshape(X.shape)
Running this gives me the following error (for the last line I entered above):
total size of new array must be unchanged
I went digging around stackoverflow, and tried using suggestions from people having similar problems. Here are the errors I get from each of those suggestions:
Changing the last line to:
Z = znew.reshape(X.shape[0])
Gives the same error.
Changing the last line to:
Z = znew.reshape(X.shape[0], len(znew))
Gives the error:
Shape of x does not match that of z: found (294, 294) instead of (294, 86436).
Changing it to:
Z = znew.reshape(X.shape, len(znew))
Gives the error:
an integer is required
Any ideas?
Well,sample code below works for me
import numpy as np
import matplotlib.pyplot as plt
xr = np.linspace(-20, 20, 100)
yr = np.linspace(-25, 25, 110)
X, Y = np.meshgrid(xr, yr)
#Z = 4*X**2 + Y**2
zr = []
for i in range(0, 110):
y = -25.0 + (50./110.)*float(i)
for k in range(0, 100):
x = -20.0 + (40./100.)*float(k)
v = 4.0*x*x + y*y
zr.append(v)
Z = np.reshape(zr, X.shape)
print(X.shape)
print(Y.shape)
print(Z.shape)
plt.contour(X, Y, Z)
plt.show()
TL;DR
import matplotlib.pyplot as plt
import numpy as np
def get_data_for_mpl(X, Y, Z):
result_x = np.unique(X)
result_y = np.unique(Y)
result_z = np.zeros((len(result_x), len(result_y)))
# result_z[:] = np.nan
for x, y, z in zip(X, Y, Z):
i = np.searchsorted(result_x, x)
j = np.searchsorted(result_y, y)
result_z[i, j] = z
return result_x, result_y, result_z
xr, yr, zr = np.genfromtxt('data.txt', unpack=True)
plt.contourf(*get_data_for_mpl(xr, yr, zr), 100)
plt.show()
Detailed answer
At the beginning, you need to find out for which values of x and y the graph is being plotted. This can be done using the numpy.unique function:
result_x = numpy.unique(X)
result_y = numpy.unique(Y)
Next, you need to create a numpy.ndarray with function values for each point (x, y) from zip(X, Y):
result_z = numpy.zeros((len(result_x), len(result_y)))
for x, y, z in zip(X, Y, Z):
i = search(result_x, x)
j = search(result_y, y)
result_z[i, j] = z
If the array is sorted, then the search in it can be performed not in linear time, but in logarithmic time, so it is enough to use the numpy.searchsorted function to search. but to use it, the arrays result_x and result_y must be sorted. Fortunately, sorting is part of the numpy.unique method and there are no additional actions to do. It is enough to replace the search (this method is not implemented anywhere and is given simply as an intermediate step) method with np.searchsorted.
Finally, to get the desired image, it is enough to call the matplotlib.pyplot.contour or matplotlib.pyplot.contourf method.
If the function value does not exist for (x, y) for all x from result_x and all y from result_y, and you just want to not draw anything, then it is enough to replace the missing values with NaN. Or, more simply, create result_z as numpy.ndarray` from NaN and then fill it in:
result_z = numpy.zeros((len(result_x), len(result_y)))
result_z[:] = numpy.nan

Symmetric streamplot with matplotlib

I'm trying to plot the streamlines of a magnetic field around a sphere using matplotlib, and it does work quite nicely. However, the resulting image is not symmetric, but it should be (I think).
This is the code used to generate the image. Excuse the length, but I thought it would be better than just posting a non-working snippet. Also, it's not very pythonic; that's because I converted it from Matlab, which was easier than I expected.
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
def cart2spherical(x, y, z):
r = np.sqrt(x**2 + y**2 + z**2)
phi = np.arctan2(y, x)
theta = np.arccos(z/r)
if r == 0:
theta = 0
return (r, theta, phi)
def S(theta, phi):
S = np.array([[np.sin(theta)*np.cos(phi), np.cos(theta)*np.cos(phi), -np.sin(phi)],
[np.sin(theta)*np.sin(phi), np.cos(theta)*np.sin(phi), np.cos(phi)],
[np.cos(theta), -np.sin(theta), 0]])
return S
def computeB(r, theta, phi, a=1, muR=100, B0=1):
delta = (muR - 1)/(muR + 2)
if r > a:
Bspherical = B0*np.array([np.cos(theta) * (1 + 2*delta*a**3 / r**3),
np.sin(theta) * (delta*a**3 / r**3 - 1),
0])
B = np.dot(S(theta, phi), Bspherical)
else:
B = 3*B0*(muR / (muR + 2)) * np.array([0, 0, 1])
return B
Z, X = np.mgrid[-2.5:2.5:1000j, -2.5:2.5:1000j]
Bx = np.zeros(np.shape(X))
Bz = np.zeros(np.shape(X))
Babs = np.zeros(np.shape(X))
for i in range(len(X)):
for j in range(len(Z)):
r, theta, phi = cart2spherical(X[0, i], 0, Z[j, 0])
B = computeB(r, theta, phi)
Bx[i, j], Bz[i, j] = B[0], B[2]
Babs[i, j] = np.sqrt(B[0]**2 + B[1]**2 + B[2]**2)
fig=plt.figure()
ax=fig.add_subplot(111)
plt.streamplot(X, Z, Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3,
minlength=0.9, arrowstyle='-')
ax.add_patch(Circle((0, 0), radius=1, facecolor='none', linewidth=2))
plt.axis('equal')
plt.axis('off')
fig.savefig('streamlines.pdf', transparent=True, bbox_inches='tight', pad_inches=0)
First of all, for curiosity, why would you want to plot symmetric data? Why plotting half of isn't fine?
Said that, this is a possible hack. You can use mask arrays as Hooked suggested to plot half of it:
mask = X>0
BX_OUT = Bx.copy()
BZ_OUT = Bz.copy()
BX_OUT[mask] = None
BZ_OUT[mask] = None
res = plt.streamplot(X, Z, BX_OUT, BZ_OUT, color='k',
arrowstyle='-',linewidth=1,density=2)
then you save in res the result from streamplot, extract the lines and plot them with the opposite X coordinate.
lines = res.lines.get_paths()
for l in lines:
plot(-l.vertices.T[0],l.vertices.T[1],'k')
I used this hack to extract streamlines and arrows from a 2D plot, then apply a 3D transformation and plot it with mplot3d. A picture is in one of my questions here.
Quoting from the documentation:
density : float or 2-tuple
Controls the closeness of streamlines. When density = 1,
the domain is divided into
a 25x25 grid—density linearly scales this grid.
Each cell in the grid can have, at most, one traversing streamline.
For different densities in each direction, use [density_x, density_y].
so you are getting aliasing effects between the cells it uses to decide where the stream lines are, and the symmetries of your problem. You need to carefully choose your grid size (of the data) and the density.
It is also sensitive to where the box boundaries are relative to the top of the sphere. Is the center of your sphere on a data grid point or between the data grid points? If it is on a grid point then the box that contains the center point will be different than the boxes adjacent to it.
I am not familiar with exactly how it decides which stream lines to draw, but I could imagine that it is some sort of greedy algorithm and hence will give different results walking towards the high density region and away density region.
To be clear, you issue is not that the stream lines are wrong, they are valid stream lines, it is that you find the result not aesthetically pleasing.
Use a mask to separate the two regions of interest:
mask = np.sqrt(X**2+Z**2)<1
BX_OUT = Bx.copy()
BZ_OUT = Bz.copy()
BX_OUT[mask] = None
BZ_OUT[mask] = None
plt.streamplot(X, Z, BX_OUT, BZ_OUT, color='k',
arrowstyle='-', density=2)
BX_IN = Bx.copy()
BZ_IN = Bz.copy()
BX_IN[~mask] = None
BZ_IN[~mask] = None
plt.streamplot(X, Z, BX_IN, BZ_IN, color='r',
arrowstyle='-', density=2)
The resulting plot isn't exactly symmetric, but by giving the algorithm a hint, it's far closer than what you had before. Play with the density of the grid via meshgrid and the density parameter to achieve the effect you are looking for.
Use physics, instead... The magnetic field is symmetrical with respect to the z (vertical) axis! So you just need two streamplot's:
plt.streamplot(X, Z, Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3, minlength=0.9, arrowstyle='-')
plt.streamplot(-X, Z, -Bx, Bz, color='k', linewidth=0.8*Babs, density=1.3, minlength=0.9, arrowstyle='-')

Mayavi: interpolate face colors in triangular_mesh

I have pieced together the
following code to plot a triangular mesh with the colors specified by an
additional scalar function:
#! /usr/bin/env python
import numpy as np
from mayavi import mlab
# Create cone
n = 8
t = np.linspace(-np.pi, np.pi, n)
z = np.exp(1j*t)
x = z.real.copy()
y = z.imag.copy()
z = np.zeros_like(x)
triangles = [(0, i, i+1) for i in range(n)]
x = np.r_[0, x]
y = np.r_[0, y]
z = np.r_[1, z]
t = np.r_[0, t]
# These are the scalar values for each triangle
f = np.mean(t[np.array(triangles)], axis=1)
# Plot it
mesh = mlab.triangular_mesh(x, y, z, triangles,
representation='wireframe',
opacity=0)
cell_data = mesh.mlab_source.dataset.cell_data
cell_data.scalars = f
cell_data.scalars.name = 'Cell data'
cell_data.update()
mesh2 = mlab.pipeline.set_active_attribute(mesh,
cell_scalars='Cell data')
mlab.pipeline.surface(mesh2)
mlab.show()
This works reasonably well. However, instead of having every triangle
with a uniform color and sharp transitions between the triangles, I'd
much rather have a smooth interpolation over the entire surface.
Is there a way to do that?
I think you want to use point data instead of cell data. With cell data, a single scalar value is not localized to any point. It is assigned to the entire face. It looks like you just want to assign the t data to the vertices instead. The default rendering of point scalars will smoothly interpolate across each face.
point_data = mesh.mlab_source.dataset.point_data
point_data.scalars = t
point_data.scalars.name = 'Point data'
point_data.update()
mesh2 = mlab.pipeline.set_active_attribute(mesh,
point_scalars='Point data')

Categories

Resources