Best fitting rectangular mesh to a smooth 3D surface - python

G'day, I'm struggling to find a way to create a rectangular mesh that best fits a smooth 3D surface. Particularly I have a model of an earthquake fault shown in this plot.
These are the depth contours to the fault. I want to find a rectangular mesh of defined dimension (say 10x10km) that best fits the surface. It doesn't have to (and it can't) be exactly on the surface, just the closest possible and it HAS to be a rectangle, not just a quadrangle. I have the nodes that define the surface and I can easily interpolate them.
Python solutions are welcome or suggestions on open-source code that my tackle this. I've tried commercial meshers (ABAQUS) but they always return quadrangles. I haven't been able to figure this out so any hints are appreciated.

If you have the nodes that define the surface, that means you have an irregular grid of coordinates and corresponding values. So you can generate a triangulation from this (most likely the tool you're using to show these filled contours uses the same behind the screens).
Matplotlib has two very useful classes that can convert a triangulation to a rectilinear grid (the more generic form of a rectangular grid): LinearTriInterpolator and CubicTriInterpolator. They are being used in this matplotlib example.
These are the basic steps from that same example, annotated by me, but credit goes to the matplotlib contributors:
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import numpy as np
# Create triangulation.
coords, earthquake_fault = get_coordinate_data() # to be filled in by you
x = coords['x']
y = coords['y']
triang = mtri.Triangulation(x, y)
# Interpolate to regularly-spaced quad grid.
z = earthquake_fault # the "height" data
xi, yi = np.meshgrid(np.linspace(x.min(), x.max() 20), np.linspace(y.min(), y.max(), 20))
interp_lin = mtri.LinearTriInterpolator(triang, z)
zi_lin = interp_lin(xi, yi)
# Plot the triangulation.
plt.subplot(121)
plt.tricontourf(triang, z)
plt.triplot(triang, 'ko-')
plt.title('Triangular grid')
# Plot linear interpolation to quad grid.
plt.subplot(122)
plt.contourf(xi, yi, zi_lin)
plt.title('Rectangular grid')

Related

Matplotlib plot_surface: How to convert 1D arrays to required 2D input?

Maybe this question is a duplicate because I can imagine that many people face this problem. Forgive me if so.
I want to plot a sphere in Matplotlib 3D. For that, I have a bunch of xyz coordinates. When I plot it with plot_trisurf, I get this:
So I wanted to try plot_surface, but then I get the error ValueError: Argument Z must be 2-dimensional.
This post explains why the input for plot_surface is 2D.
My question ist: How can I convert my regular xyz coordinates into the format plot_surface needs?
Edit:
Okay, I understood that 3-tuples can be differently interpreted. Is there a way then to use plot_trisurf with some kind of polar coordinates, so that it doesn't interpolate "through the xy plane" but from the coordinate origin, spherically?
If your points are created in a mesh-like way, it is best to create mesh at the same time, such as in this post.
It seems plot_trisurf creates a mesh for an open surface (like a rectangular table cloth) but not for a closed surface.
If the points aren't nicely organized, but you know all points lie on a convex 3D surface (e.g. a sphere), you can calculate the 3D convex hull and draw that.
The code below does just that. Note that some triangles look darker and some lighter. This is because the triangles returned by ConvexHull aren't nicely oriented (so that e.g. a clockwise orientation would indicate the outside face of the polygon). For that you'd need to calculate the surface normal for each triangle and reverse the triangle in case the dot product of that normal with the center of the triangle would be negative (supposing 0,0,0 lies inside the sphere).
If you need more 3D plotting power, the Mayawi library would be more appropriate.
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.spatial import ConvexHull
import numpy as np
xyz = np.random.randn(3, 50) # random 3D points
xyz /= np.linalg.norm(xyz, axis=0) # project each point on a unit sphere
fig = plt.figure()
ax = fig.gca(projection='3d')
hull = ConvexHull(xyz.T)
ax.plot_trisurf(*xyz, triangles=hull.simplices, linewidth=0.2, antialiased=True)
plt.show()

Plotting patches of random shapes with matplotlib

This is my target to plot:
Several ellipses which are not regular shape.
(source: clouddn.com)
I was thinking about generating some random number as vertices location.
But it can only build a polygon. So, how to plot several arcs and make them close up?
To create any arbitrary shape, you will need to use the matplotlib.patches.Polygon class and just provide enough x,y samples to make it appear as smooth of a path as necessary (at the end of the day it's still straight line segments when you zoom in close enough).
If you only have a few points, you can use one of many interpolation methods (such as scipy.interpolate.spline) to create a smooth interpolant of the data that you can then feed to the Polygon constructor.
Here is a simple example creating a circle using the Polygon class by supplying x,y points around the circle.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
# Circle coordinates (100 points around the circle)
t = np.linspace(0, 2 * np.pi, 100).reshape(100,1)
coords = np.concatenate((np.cos(t), np.sin(t)), axis=1)
ax = plt.axes()
polygons = [];
polygons.append(Polygon(coords))
p = PatchCollection(polygons, alpha=0.4)
ax.add_collection(p)
ax.axis('equal')
Sonds just like the example in the official documentation:
http://matplotlib.org/examples/pylab_examples/ellipse_demo.html
the main part, they just construct a list of the ellipses:
ells = [Ellipse(xy=rnd.rand(2)*10, width=rnd.rand(), height=rnd.rand(), angle=rnd.rand()*360) for i in range(250)]
...or did I miss your point? :)

Best way to plot a 3D matrix in python

I am trying to visualize 3D data. This is a full 3D matrix: each (x,y,z) coordinate has a value, unlike a surface or a collection of individual data vectors. The way I am trying to do this is to plot an opaque cube, where each edge of the cube shows the sum of the data over the orthogonal dimension.
Some example data -- basically, a blob centered at (3,5,7):
import numpy as np
(x,y,z) = np.mgrid[0:10,0:10, 0:10]
data = np.exp(-((x-3)**2 + (y-5)**2 + (z-7)**2)**(0.5))
edge_yz = np.sum(data,axis=0)
edge_xz = np.sum(data,axis=1)
edge_xy = np.sum(data,axis=2)
So the idea would be here to generate a 3D plot that showed a cube; each surface of the cube would show the appropriate 2D matrix edge_*. This would be like plotting 3 4-sided polygons at the appropriate 3D positions (or 6 if you did the back sides of the cube as well) except that each polygon is actually a matrix of values to be plotted in color.
My best approximation at the moment is to compute larger matrices that contained skewed versions of edge, and concatenate these into a single, larger 2D matrix, and imshow() that larger matrix. Seems pretty clumsy, and does a lot of work that some engine in matplotlib or m3plot or something I'm sure already does. It also only works to view a static image at a single view angle, but that's not something I need to overcome at the moment.
Is there a good way to plot these cube edges in a true 3D plot using an existing python tool? Is there a better way to plot a 3D matrix?
Falko's suggestion to use contourf works with a bit of finagling. It's a bit limited since at least my version of contourf has a few bugs where it sometimes renders one of the planes in front of other planes it should be behind, but for now only plotting either the three front or three back sides of the cube will do:
import numpy as np
import math
import matplotlib.pyplot as plot
import mpl_toolkits.mplot3d.axes3d as axes3d
def cube_marginals(cube, normalize=False):
c_fcn = np.mean if normalize else np.sum
xy = c_fcn(cube, axis=0)
xz = c_fcn(cube, axis=1)
yz = c_fcn(cube, axis=2)
return(xy,xz,yz)
def plotcube(cube,x=None,y=None,z=None,normalize=False,plot_front=False):
"""Use contourf to plot cube marginals"""
(Z,Y,X) = cube.shape
(xy,xz,yz) = cube_marginals(cube,normalize=normalize)
if x == None: x = np.arange(X)
if y == None: y = np.arange(Y)
if z == None: z = np.arange(Z)
fig = plot.figure()
ax = fig.gca(projection='3d')
# draw edge marginal surfaces
offsets = (Z-1,0,X-1) if plot_front else (0, Y-1, 0)
cset = ax.contourf(x[None,:].repeat(Y,axis=0), y[:,None].repeat(X,axis=1), xy, zdir='z', offset=offsets[0], cmap=plot.cm.coolwarm, alpha=0.75)
cset = ax.contourf(x[None,:].repeat(Z,axis=0), xz, z[:,None].repeat(X,axis=1), zdir='y', offset=offsets[1], cmap=plot.cm.coolwarm, alpha=0.75)
cset = ax.contourf(yz, y[None,:].repeat(Z,axis=0), z[:,None].repeat(Y,axis=1), zdir='x', offset=offsets[2], cmap=plot.cm.coolwarm, alpha=0.75)
# draw wire cube to aid visualization
ax.plot([0,X-1,X-1,0,0],[0,0,Y-1,Y-1,0],[0,0,0,0,0],'k-')
ax.plot([0,X-1,X-1,0,0],[0,0,Y-1,Y-1,0],[Z-1,Z-1,Z-1,Z-1,Z-1],'k-')
ax.plot([0,0],[0,0],[0,Z-1],'k-')
ax.plot([X-1,X-1],[0,0],[0,Z-1],'k-')
ax.plot([X-1,X-1],[Y-1,Y-1],[0,Z-1],'k-')
ax.plot([0,0],[Y-1,Y-1],[0,Z-1],'k-')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plot.show()
plot_front=True
plot_front=False
Other data (not shown)
Take a look at MayaVI. The contour3d() function may be what you want.
Here's an answer I gave to a similar question with an example of the code and resulting plot https://stackoverflow.com/a/24784471/3419537

Matplotlib tripcolor bug?

I want to use tripcolor from matplotlib.pyplot to view the colored contours of some of my data.
The data is extracted from an XY plane at z=cst using Paraview. I directly export the data in csv from Paraview which triangulates the plane for me.
The problem is that depending on the plane position (ie the mesh) tripcolor gives me sometimes good or bad results.
Here is a simple example code and results to illustrate it:
Code
import matplotlib.pyplot as plt
import numpy as np
p,u,v,w,x,y,z = np.loadtxt('./bad.csv',delimiter=',',skiprows=1,usecols=(0,1,2,3,4,5,6),unpack=True)
NbLevels = 256
plt.figure()
plt.gca().set_aspect('equal')
plt.tripcolor(x,y,w,NbLevels,cmap=plt.cm.hot_r,edgecolor='black')
cbar = plt.colorbar()
cbar.set_label('Velocity magnitude',labelpad=10)
plt.show()
Results with tripcolor
Here is the file that causes the problem.
I've heard that matplotlib's tripcolor is sometimes buggy, so is it a bug or not ?
As highlighted by #Hooked this is the normal behaviour for a Delaunay triangulation.
To remove unwanted triangles you should provide your own Triangulation by passing explicitly the triangles.
This is quite easy in your case as your data is almost structured: I suggest performing a Delaunay triangulation in the plane (r, theta) then passing these triangles to the initial (x, y) arrays. You can make use of the the built-in TriAnalyzer class to remove very flat triangles from the (r, theta) triangulation (they might exists due to round-off errors).
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.tri as mtri
p,u,v,w,x,y,z = np.loadtxt('./bad.csv',delimiter=',',skiprows=1,usecols=(0,1,2,3,4,5,6),unpack=True)
r = np.sqrt(y**2 + x**2)
tan = (y / x)
aux_tri = mtri.Triangulation(r/np.max(r), tan/np.max(tan))
triang = mtri.Triangulation(x, y, aux_tri.triangles)
triang.set_mask(mtri.TriAnalyzer(aux_tri).get_flat_tri_mask())
NbLevels = 256
plt.figure()
plt.gca().set_aspect('equal')
plt.tripcolor(triang, w, NbLevels, cmap=plt.cm.jet, edgecolor='black')
cbar = plt.colorbar()
cbar.set_label('Velocity magnitude',labelpad=10)
plt.show()
It's probably because the Delaunay triangulation called by Paraview created a convex hull of the points (as it should). To test this, I used matplotlib.tri.Triangulation and plotted the resulting mesh from the x-y values:
import matplotlib.tri as tri
plt.scatter(x,y)
w[:] = 1
triang = tri.Triangulation(x, y)
plt.tripcolor(triang,w,alpha=.2)
which shows the same effect. It may be possible to remove the unwanted triangles from the mesh, either by hand, or using a non-convex boundary finder.

Contour irregular data within polygon

I need to create filled contour plots of sea surface temperature (SST) data within a polygon, however I am not sure the best way to do this. I have three 1D arrays containing data for X, Y, and SST which I plot using the following to create the attached plot:
p=PatchCollection(mypatches,color='none', alpha=1.0,edgecolor="purple",linewidth=2.0)
levels=np.arange(SST.min(),SST.max(),0.2)
datamap=mymap.scatter(x,y,c=SST, s=55, vmin=-2,vmax=3,alpha=1.0)
I would like to be able to plot these data as filled contours (contourf instead of scatter) that are constrained (clipped) within the polygon boundaries (the purple line). Suggestions for how to achieve this are greatly appreciated.
Update:
I had originally tried griddata, but could not get it to work properly. However, based on the answer provided by #eatHam I decided to try again. I could not get my scipy griddata to work as it kept hanging at the gridding when selecting method 'cubic', however when I switched to matplotlib.mlab.griddata and used 'linear' interpolation it worked. The suggestion for masking the boundaries provided a very coarse and not as exact solution as i would prefer.
I searched for options on how to clip contours in matplotlib and I found an answer by #pelson at this link. I tried the suggested solution implied in: "The contour set itself does not have a set_clip_path method but you can iterate over each of the contour collections and set their respective clip paths". My new and final solution looks like this (see plot below):
p=PatchCollection(mypatches,color='none', alpha=1.0,edgecolor="purple",linewidth=2.0)
levels=np.arange(SST.min(),SST.max(),0.2)
grid_x, grid_y = np.mgrid[x.min()-0.5*(x.min()):x.max()+0.5*(x.max()):200j,
y.min()-0.5*(y.min()):y.max()+0.5*(y.max()):200j]
grid_z = griddata(x,y,SST,grid_x,grid_y)
cs=mymap.contourf(grid_x, grid_y, grid_z)
for poly in mypatches:
for artist in ax.get_children():
artist.set_clip_path(poly)
ax.add_patch(poly)
mymap.drawcountries()
mymap.drawcoastlines()
mymap.fillcontinents(color='lightgrey',lake_color='none')
mymap.drawmapboundary(fill_color='none')
This solution could also be improved particularily in terms of extrapolating the extreme edges in the North. Suggestions for how to really 'fill-in' the full polygon are appreciated. I also would like to understand why mlab worked and scipy not.
I would interpolate the data using scipy.griddata. You can set the region outside of your area (mypatches) to np.nan. And then just use pyplot.contour to plot it.
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import griddata
def sst_data(x, y):
return x*(1-x)*np.cos(4*np.pi*x) * np.sin(4*np.pi*y**2)**2
#replace with ...
x = np.random.rand(1000) #... your x
y = np.random.rand(1000) #... your y
sst = sst_data(x, y) #... your sst
# interpolate to a grid
grid_x, grid_y = np.mgrid[0:1:100j, 0:1:200j]
grid_z = griddata((x,y), sst, (grid_x, grid_y), method='cubic')
# mask out the area outside of your region
nr, nc = grid_z.shape
grid_z[-nr//3:, -nc//3:] = np.nan
plt.contourf(grid_x, grid_y, grid_z)
plt.show()
EDIT: Changed variable name in the plt.contourf() call (was ..(grid_z, grid_y, grid_z))

Categories