Rotate square to be normal to a vector - python
Win 7, x64, Python 2.7
I'm trying to rotate a square that is initially in the xz plane so that its normal aligns with a given 3D vector. Also I am translating the square to the start of the vector but that isnt a problem.
The path I have taken is as follows,
1) Find the axis of rotation via the cross product of the given vector & the square's normal, a unit vector in the y direction in this case.
2) Find the angle of rotation via the dot product of the given vector and the square's normal.
3) Build appropriate rotation matrix.
4) Apply rotation matrix to the vertices of the square.
5) Translate to the start of the given vector.
The code..
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import math
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
na = np.array
def rotation_matrix(axis, theta):
"""
Return the rotation matrix associated with counterclockwise rotation about
the given axis by theta radians.
"""
axis = np.asarray(axis)
axis = axis/math.sqrt(np.dot(axis, axis))
a = math.cos(theta/2.0)
b, c, d = -axis*math.sin(theta/2.0)
aa, bb, cc, dd = a*a, b*b, c*c, d*d
bc, ad, ac, ab, bd, cd = b*c, a*d, a*c, a*b, b*d, c*d
return np.array([[aa+bb-cc-dd, 2*(bc+ad), 2*(bd-ac)],
[2*(bc-ad), aa+cc-bb-dd, 2*(cd+ab)],
[2*(bd+ac), 2*(cd-ab), aa+dd-bb-cc]])
edgeLen = 4.0 # length of square side
pos = na([2.0,2.0,2.0]) # starting point of vector
dirc = na([6.0,6.0,6.0]) # direction of vector
Ux = na([1.0,0.0,0.0]) # unit basis vectors
Uy = na([0.0,1.0,0.0])
Uz = na([0.0,0.0,1.0])
x = pos[0]
y = pos[1]
z = pos[2]
# corner vertices of square in xz plane
verts = na([[edgeLen/2.0, 0, edgeLen/2.0],
[edgeLen/2.0, 0, -edgeLen/2.0],
[-edgeLen/2.0, 0, -edgeLen/2.0],
[-edgeLen/2.0, 0, edgeLen/2.0]])
# For axis & angle of rotation
dirMag = np.linalg.norm(dirc)
axR = np.cross(dirc, Uy)
theta = np.arccos((np.dot(dirc, Uy) / dirMag))
Rax = rotation_matrix(axR, theta) # rotation matrix
# rotate vertices
rotVerts = na([0,0,0])
for v in verts:
temp = np.dot(Rax, v)
temp = na([temp[0]+x, temp[1]+y, temp[2]+z])
rotVerts = np.vstack((rotVerts, temp))
rotVerts = np.delete(rotVerts, rotVerts[0], axis=0)
# plot
# oringinal square
ax.scatter(verts[:,0], verts[:,1], verts[:,2], s=10, c='r', marker='o')
ax.plot([verts[0,0], verts[1,0]], [verts[0,1], verts[1,1]], [verts[0,2], verts[1,2]], color='g', linewidth=1.0)
ax.plot([verts[1,0], verts[2,0]], [verts[1,1], verts[2,1]], [verts[1,2], verts[2,2]], color='g', linewidth=1.0)
ax.plot([verts[2,0], verts[3,0]], [verts[2,1], verts[3,1]], [verts[2,2], verts[3,2]], color='g', linewidth=1.0)
ax.plot([verts[0,0], verts[3,0]], [verts[0,1], verts[3,1]], [verts[0,2], verts[3,2]], color='g', linewidth=1.0)
# rotated & translated square
ax.scatter(rotVerts[:,0], rotVerts[:,1], rotVerts[:,2], s=10, c='b', marker='o')
ax.plot([rotVerts[0,0], rotVerts[1,0]], [rotVerts[0,1], rotVerts[1,1]], [rotVerts[0,2], rotVerts[1,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[1,0], rotVerts[2,0]], [rotVerts[1,1], rotVerts[2,1]], [rotVerts[1,2], rotVerts[2,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[2,0], rotVerts[3,0]], [rotVerts[2,1], rotVerts[3,1]], [rotVerts[2,2], rotVerts[3,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[0,0], rotVerts[3,0]], [rotVerts[0,1], rotVerts[3,1]], [rotVerts[0,2], rotVerts[3,2]], color='b', linewidth=1.0)
# vector
ax.plot([pos[0], pos[0]+dirc[0]], [pos[1], pos[1]+dirc[1]], [pos[1], pos[1]+dirc[1]], color='r', linewidth=1.0)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
This gives the following output..
The green square is the original in the xz plane, the blue square the transformed square & the given vector is in red.
As you can see its well off. After many hours pouring through similar questions & replies, I am still none the wiser as to why this does not work.
So what am I missing here?
EDIT: After pouring over the Euler Angles link, given by El Dude in the comments below, I tried the following....
Defined the square in yz plane of a static frame of reference xyz with basis vectors Ux, Uy & Uz
Used a direction vector 'dirVec' as the normal for the plane I want to rotate my square into.
I decided to use the x-convention and the ZXZ rotation matrix as discribed in Euler angles link.
Steps I have taken,
1) Create a rotated frame with Tx, Ty & Tz as basis vectors;
Tx = dirVec
Ty = Tx cross Uz (Tx not allowed to parallel to Uz)
Tz = Ty cross Tx
2) Defined a Node Line, a vector along the intersection of the planes UxUy & TxTy by taking the cross product of Uz & Tz
3) Defined the Euler angles as per the definitions in the above link
4) Defined the ZXZ rotation matrix as per the above link
5) Applied rotation matrix to coordinates of square's vertices
It doesn't work, something odd is happening, no matter what the value of 'dirVec' alpha always comes out as 0.
Is there something obvious going on that I'm just missing?
Here's the amended code...
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import math
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
na = np.array
def rotation_ZXZ(alpha=0.0, beta=0.0, gamma=0.0):
"""
Return ZXZ rotaion matrix
"""
a = alpha
b = beta
g = gamma
ca = np.cos(a)
cb = np.cos(b)
cg = np.cos(g)
sa = np.sin(a)
sb = np.sin(b)
sg = np.sin(g)
return np.array([[(ca*cg-cb*sa*sg), (-ca*sg-cb*cg*sa), sa*sb],
[(cg*sa+ca*cb*sg), (ca*cb*cg-sa*sg), -ca*sb],
[sb*sg, cg*sb, cb]])
def rotated_axes(vector=[0,1,0]):
"""
Return unit basis vectors for rotated frame
"""
vx = np.asarray(vector) / np.linalg.norm(vector)
if vx[1] != 0 or vx[2] != 0:
U = na([1.0, 0.0, 0.0])
else:
U = na([0.0, 1.0, 0.0])
vz = np.cross(vx, U)
vz = vz / np.linalg.norm(vz)
vy = np.cross(vx, vz)
vy = vy / np.linalg.norm(vy)
vx = bv(vx[0], vx[1], vx[2])
vy = bv(vy[0], vy[1], vy[2])
vz = bv(vz[0], vz[1], vz[2])
return vx, vy, vz
def angle_btw_vectors(v1=[1,0,0], v2=[0,1,0]):
"""
Return the angle, in radians, between 2 vectors
"""
v1 = np.asarray(v1)
v2 = np.asarray(v2)
mags = np.linalg.norm(v1) * np.linalg.norm(v2)
return np.arccos(np.dot(v1, v2) / mags)
edgeLen = 4.0 # length of square side
dirVec = na([4,4,4]) # direction of given vector
pos = na([0.0, 0.0, 0.0]) # starting point of given vector
x = pos[0]
y = pos[1]
z = pos[2]
Ux = na([1,0,0]) # Unit basis vectors for static frame
Uy = na([0,1,0])
Uz = na([0,0,1])
Tx, Ty, Tz = rotated_axes(dirVec) # Unit basis vectors for rotated frame
# where Tx = dirVec / |dirVec|
nodeLine = np.cross(Uz, Tz) # Node line - xy intersect XY
alpha = angle_btw_vectors(Ux, nodeLine) #Euler angles
beta = angle_btw_vectors(Uz, Tz)
gamma = angle_btw_vectors(nodeLine, Tx)
Rzxz = rotation_ZXZ(alpha, beta, gamma) # Rotation matrix
print '--------------------------------------'
print 'Tx: ', Tx
print 'Ty: ', Ty
print 'Tz: ', Tz
print 'Node line: ', nodeLine
print 'Tx.dirVec: ', np.dot(Tx, (dirVec / np.linalg.norm(dirVec)))
print 'Ty.dirVec: ', np.dot(Ty, dirVec)
print 'Tz.dirVec: ', np.dot(Tz, dirVec)
print '(Node Line).Tx: ', np.dot(Tx, nodeLine)
print 'alpha: ', alpha * 180 / np.pi
print 'beta: ', beta * 180 / np.pi
print 'gamma: ', gamma * 180 / np.pi
#print 'Rzxz: ', Rxzx
# corner vertices of square in yz plane
verts = na([[0, edgeLen/2.0, edgeLen/2.0],
[0, edgeLen/2.0, -edgeLen/2.0],
[0, -edgeLen/2.0, -edgeLen/2.0],
[0, -edgeLen/2.0, edgeLen/2.0]])
rotVerts = na([0,0,0])
for v in verts:
temp = np.dot(Rzxz, v)
temp = na([temp[0]+x, temp[1]+y, temp[2]+z])
rotVerts = np.vstack((rotVerts, temp))
rotVerts = np.delete(rotVerts, rotVerts[0], axis=0)
# plot
# oringinal square
ax.scatter(verts[:,0], verts[:,1], verts[:,2], s=10, c='g', marker='o')
ax.plot([verts[0,0], verts[1,0]], [verts[0,1], verts[1,1]], [verts[0,2], verts[1,2]], color='g', linewidth=1.0)
ax.plot([verts[1,0], verts[2,0]], [verts[1,1], verts[2,1]], [verts[1,2], verts[2,2]], color='g', linewidth=1.0)
ax.plot([verts[2,0], verts[3,0]], [verts[2,1], verts[3,1]], [verts[2,2], verts[3,2]], color='g', linewidth=1.0)
ax.plot([verts[0,0], verts[3,0]], [verts[0,1], verts[3,1]], [verts[0,2], verts[3,2]], color='g', linewidth=1.0)
# rotated & translated square
ax.scatter(rotVerts[:,0], rotVerts[:,1], rotVerts[:,2], s=10, c='b', marker='o')
ax.plot([rotVerts[0,0], rotVerts[1,0]], [rotVerts[0,1], rotVerts[1,1]], [rotVerts[0,2], rotVerts[1,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[1,0], rotVerts[2,0]], [rotVerts[1,1], rotVerts[2,1]], [rotVerts[1,2], rotVerts[2,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[2,0], rotVerts[3,0]], [rotVerts[2,1], rotVerts[3,1]], [rotVerts[2,2], rotVerts[3,2]], color='b', linewidth=1.0)
ax.plot([rotVerts[0,0], rotVerts[3,0]], [rotVerts[0,1], rotVerts[3,1]], [rotVerts[0,2], rotVerts[3,2]], color='b', linewidth=1.0)
# Rotated reference coordinate system
ax.plot([pos[0], pos[0]+Tx[0]], [pos[1], pos[1]+Tx[1]], [pos[2], pos[2]+Tx[2]], color='r', linewidth=1.0)
ax.plot([pos[0], pos[0]+Ty[0]], [pos[1], pos[1]+Ty[1]], [pos[1], pos[2]+Ty[2]], color='b', linewidth=1.0)
ax.plot([pos[0], pos[0]+Tz[0]], [pos[1], pos[1]+Tz[1]], [pos[1], pos[2]+Tz[2]], color='g', linewidth=1.0)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
Here's a solution that I came up with - it should work, although there wasn't a whole lot of testing. The solution is somewhat more general, as it would work for any 2D object of any orientation, the only thing you have to adjust are the vertices stored in obj (this could be done better but here I just created a list of points by hand).
Note, that I defined mObj as the "center" of the object - this does not change the functionality but is the anchor point of the normal vector that is displayed.
Here's some explanation for the math:
What we need to do is to find the right rotation axis and angle, such that we only need one matrix multiplication (in principle you could use the Euler angles which would be an equivalent solution). The angle is easy, since it is given by the dot-product:
dot(a, b) = |a| |b| * cos(theta)
where theta is the angle between the vector a and b. To find the rotation axis, we can use the normal vector of the plane spanned by a and b, i.e. use the cross product and normalize it:
rotAxis = cross(a, b) / |cross(a, b)|
Note that this vector is orthogonal to a and b, hence the axis we are looking for.
Hope this helps.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def rotateVector3D(v, theta, axis):
""" Takes a three-dimensional vector v and rotates it by the angle theta around the specified axis.
"""
return np.dot(rotationMatrix3D(theta, axis), v)
def rotationMatrix3D(theta, axis):
""" Return the rotation matrix associated with counterclockwise rotation about
the given axis by theta radians.
"""
axis = np.asarray(axis) / np.sqrt(np.dot(axis, axis))
a = np.cos(theta/2.0)
b, c, d = -axis*np.sin(theta/2.0)
aa, bb, cc, dd = a**2, b**2, c**2, d**2
bc, ad, ac, ab, bd, cd = b*c, a*d, a*c, a*b, b*d, c*d
return np.array([[aa+bb-cc-dd, 2*(bc+ad), 2*(bd-ac)],
[2*(bc-ad), aa+cc-bb-dd, 2*(cd+ab)],
[2*(bd+ac), 2*(cd-ab), aa+dd-bb-cc]])
def drawObject(ax, pts, color="red"):
""" Draws an object on a specified 3D axis with points and lines between consecutive points.
"""
map(lambda pt: ax.scatter(*pt, s=10, color=color), pts)
for k in range(len(pts)-1):
x, y, z = zip(*pts[k:k+2])
ax.plot(x, y, z, color=color, linewidth=1.0)
x, y, z = zip(*[pts[-1],pts[0]])
ax.plot(x, y, z, color=color, linewidth=1.0)
def normalVector(obj):
""" Takes a set of points, assumed to be flat, and returns a normal vector with unit length.
"""
n = np.cross(np.array(obj[1])-np.array(obj[0]), np.array(obj[2])-np.array(obj[0]))
return n/np.sqrt(np.dot(n,n))
# Set the original object (can be any set of points)
obj = [(2, 0, 2), (2, 0, 4), (4, 0, 4), (4, 0, 2)]
mObj = (3, 0, 3)
nVecObj = normalVector(obj)
# Given vector.
vec = (6, 6, 6)
# Find rotation axis and angle.
rotAxis = normalVector([(0,0,0), nVecObj, vec])
angle = np.arccos(np.dot(nVecObj, vec) / (np.sqrt(np.dot(vec, vec)) * np.sqrt(np.dot(nVecObj, nVecObj))))
print "Rotation angle: {:.2f} degrees".format(angle/np.pi*180)
# Generate the rotated object.
rotObj = map(lambda pt: rotateVector3D(pt, angle, rotAxis), obj)
mRotObj = rotateVector3D(mObj, angle, rotAxis)
nVecRotObj = normalVector(rotObj)
# Set up Plot.
fig = plt.figure()
fig.set_size_inches(18,18)
ax = fig.add_subplot(111, projection='3d')
# Draw.
drawObject(ax, [[0,0,0], np.array(vec)/np.sqrt(np.dot(vec,vec))], color="gray")
drawObject(ax, [mObj, mObj+nVecObj], color="red")
drawObject(ax, obj, color="red")
drawObject(ax, [mRotObj, mRotObj + nVecRotObj], color="green")
drawObject(ax, rotObj, color="green")
# Plot cosmetics.
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
# Check if the given vector and the normal of the rotated object are parallel (cross product should be zero).
print np.round(np.sum(np.cross(vec, nVecRotObj)**2), 5)
Related
polar pcolormesh plot projected onto cartopy map
To simplify, as much as possible, a question I already asked, how would you OVERLAY or PROJECT a polar plot onto a cartopy map. phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector X,Y = np.meshgrid(thetas,phis) Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar')) im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto') ax.set_theta_direction(-1) ax.set_theta_offset(np.pi / 2.0) ax.grid(True) that results in Over a cartopy map like this... flatMap = ccrs.PlateCarree() resolution = '110m' fig = plt.figure(figsize=(12,6), dpi=96) ax = fig.add_subplot(111, projection=flatMap) ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180]) ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land'])) ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto') gc.collect() I'd like to project this polar plot over an arbitrary lon/lat... I can convert the polar theta/phi into lon/lat, but lon/lat coords (used on the map) are more 'cartesian like' than polar, hence you cannot just substitute lon/lat for theta/phi ... This is a conceptual problem. How would you tackle it?
Firstly, the data must be prepared/transformed into certain projection coordinates for use as input. And the instruction/option of the data's CRS must be specified correctly when used in the plot statement. In your specific case, you need to transform your data into (long,lat) values. XX = X/np.pi*180 # wrap around data in EW direction YY = Y*9 # spread across N hemisphere And plot it with an instruction transform=ccrs.PlateCarree(). ax.pcolormesh(XX,YY,Z, cmap=mpl.cm.jet_r,shading='auto', transform=ccrs.PlateCarree()) The same (XX,YY,Z) data set can be plotted on orthographic projection. Edit1 Update of the code and plots. Part 1 (Data) import matplotlib.colors import matplotlib.pyplot as plt import cartopy.crs as ccrs import numpy as np import matplotlib.pyplot as mpl import cartopy.feature as cfeature # # Part 1 # phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector X,Y = np.meshgrid(thetas,phis) Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar')) im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto') ax.set_theta_direction(-1) ax.set_theta_offset(np.pi / 2.0) ax.grid(True) Part 2 The required code and output. # # Part 2 # flatMap = ccrs.PlateCarree() resolution = '110m' fig = plt.figure(figsize=(12,6), dpi=96) ax = fig.add_subplot(111, projection=flatMap) ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', alpha=0.7, facecolor=cfeature.COLORS['land'])) ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree()) def scale_position(lat_deg, lon_deg, rad_deg): # Two operations: # 1. manipulates X,Y data and get (XX,YY) # 2. create proper projection of (XX,YY), `rotpole_proj` # Returns: XX,YY,rotpole_proj # For X data XX = X/np.pi*180 #always wrap around EW direction # For Y data # The cone data: min=0, max=10 --> (90-rad),90 # rad_deg = radius of the display area top = 90 btm = top-rad_deg YY = btm + (Y/Y.max())*rad_deg # The proper coordinate system rotpole_proj = ccrs.RotatedPole(pole_latitude=lat_deg, pole_longitude=lon_deg) # Finally, return XX,YY,rotpole_proj # Location 1 (Asia) XX1, YY1, rotpole_proj1 = scale_position(20, 100, 20) ax.pcolormesh(XX1, YY1, Z, cmap=mpl.cm.jet_r, transform=rotpole_proj1) # Location 2 (Europe) XX2, YY2, rotpole_proj2 = scale_position(62, -6, 8) ax.pcolormesh(XX2, YY2, Z, cmap=mpl.cm.jet_r, transform=rotpole_proj2) # Location 3 (N America) XX3, YY3, rotpole_proj3 = scale_position(29, -75, 30) ax.pcolormesh(XX3, YY3, Z, cmap=mpl.cm.jet_r,shading='auto', transform=rotpole_proj3) #gc.collect() plt.show()
This solution does NOT account for the projection point being at some altitude above the globe... I can do that part, so I really have trouble mapping the meshgrid to lon/lat so the work with the PREVIOUSLY GENERATES values of Z. Here's a simple mapping directly from polar to cart: X_cart = np.array([[p*np.sin(t) for p in phis] for t in thetas]).T Y_cart = np.array([[p*np.cos(t) for p in phis] for t in thetas]).T # Need to map cartesian XY to Z that is compatbile with above... Z_cart = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) # This Z does NOT map to cartesian X,Y print(X_cart.shape,Y_cart.shape,Z_cart.shape) flatMap = ccrs.PlateCarree() resolution = '110m' fig = plt.figure(figsize=(12,6), dpi=96) ax = fig.add_subplot(111, projection=flatMap) ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180]) ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land'])) im = ax.pcolormesh(X_cart*2,Y_cart*2, Z_cart, cmap=mpl.cm.jet_r, shading='auto') # c=mapper.to_rgba(Z_cart), cmap=mpl.cm.jet_r) gc.collect() Which maps the polar plot center to lon/lat (0,0): I'm close... I somehow need to move my cartesian coords to the proper lon/lat (the satellite sub-point) and then scale it appropriately. Have the set of lon/lat but I'm screwing up the meshgrid somehow... ??? The sphere_intersect() routine returns lon/lat for projection of theta/phi on the globe (that works)... The bit that doesn't work is building the meshgrid that replaces X,Y: lons = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg, lonlat=True)[0] for az in thetas for el in phis], dtype='object') lats = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg, lonlat=True)[1] for az in thetas for el in phis], dtype='object') long, latg = np.meshgrid(lons,lats) # THIS IS A PROBLEM I BELIEVE... and the pcolormesh makes a mess...
How to annotate a regression line with the proper text rotation
I have the following snippet of code to draw a best-fit line through a collections of points on a graph, and annotate it with the corresponding R2 value: import matplotlib.pyplot as plt import numpy as np import scipy.stats x = 50 * np.random.rand(20) + 50 y = 200 * np.random.rand(20) plt.plot(x, y, 'o') # k, n = np.polyfit(x, y, 1) k, n, r, _, _ = scipy.stats.linregress(x, y) line = plt.axline((0, n), slope=k, color='blue') xy = line.get_xydata() plt.annotate( f'$R^2={r**2:.3f}$', (xy[0] + xy[-1]) // 2, xycoords='axes fraction', ha='center', va='center_baseline', rotation=k, rotation_mode='anchor', ) plt.show() I have tried various different (x,y) pairs, different xycoords and other keyword parameters in annotate but I haven't been able to get the annotation to properly appear where I want it. How do I get the text annotation to appear above the line with proper rotation, located either at the middle point of the line, or at either end?
1. Annotation coordinates We cannot compute the coordinates using xydata here, as axline() just returns dummy xydata (probably due to the way matplotlib internally plots infinite lines): print(line.get_xydata()) # array([[0., 0.], # [1., 1.]]) Instead we can compute the text coordinates based on the xlim(): xmin, xmax = plt.xlim() xtext = (xmin + xmax) // 2 ytext = k*xtext + n Note that these are data coordinates, so they should be used with xycoords='data' instead of 'axes fraction'. 2. Annotation angle We cannot compute the angle purely from the line points, as the angle will also depend on the axis limits and figure dimensions (e.g., imagine the required rotation angle in a 6x4 figure vs 2x8 figure). Instead we should normalize the calculation to both scales to get the proper visual rotation: rs = np.random.RandomState(0) x = 50 * rs.rand(20) + 50 y = 200 * rs.rand(20) plt.plot(x, y, 'o') # save ax and fig scales xmin, xmax = plt.xlim() ymin, ymax = plt.ylim() xfig, yfig = plt.gcf().get_size_inches() k, n, r, _, _ = scipy.stats.linregress(x, y) plt.axline((0, n), slope=k, color='blue') # restore x and y limits after axline plt.xlim(xmin, xmax) plt.ylim(ymin, ymax) # find text coordinates at midpoint of regression line xtext = (xmin + xmax) // 2 ytext = k*xtext + n # find run and rise of (xtext, ytext) vs (0, n) dx = xtext dy = ytext - n # normalize to ax and fig scales xnorm = dx * xfig / (xmax - xmin) ynorm = dy * yfig / (ymax - ymin) # find normalized annotation angle in radians rotation = np.rad2deg(np.arctan2(ynorm, xnorm)) plt.annotate( f'$R^2={r**2:.3f}$', (xtext, ytext), xycoords='data', ha='center', va='bottom', rotation=rotation, rotation_mode='anchor', )
Python - find closest point to 3D point on 3D spline
I have 2 arrays with 3D points (name, X, Y, Z). First array contains reference points, through which I'm drawing spline. Second array contains measured points, from which I need to calculate normals to spline and get the coordinates of the normal on spline (I need to calculate the XY and height standard deviations of the measured points). This is the test data (in fact, I have several thousand points): 1st array - reference points/ generate spline: r1,1.5602,6.0310,4.8289 r2,1.6453,5.8504,4.8428 r3,1.7172,5.6732,4.8428 r4,1.8018,5.5296,4.8474 r5,1.8700,5.3597,4.8414 2nd array - measured points: m1, 1.8592, 5.4707, 4.8212 m2, 1.7642, 5.6362, 4.8441 m3, 1.6842, 5.7920, 4.8424 m4, 1.6048, 5.9707, 4.8465 The code I wrote, to read the data, calculate spline (using scipy) and display it via matplotlib: import numpy as np import matplotlib.pyplot as plt from scipy import interpolate # import measured points filename = "measpts.csv" meas_pts = np.genfromtxt(filename, delimiter=',') # import reference points filename = "refpts.csv" ref = np.genfromtxt(filename, delimiter=',') # divide data to X, Y, Z x = ref[:, 2] y = ref[:, 1] z = ref[:, 3] # spline interpolation tck, u = interpolate.splprep([x, y, z], s=0) u_new = np.linspace(u.min(), u.max(), 1000000) x_new, y_new, z_new = interpolate.splev(u_new, tck, der=0) xs = tck[1][0] ys = tck[1][1] zs = tck[1][2] # PLOT 3D fig = plt.figure() ax3d = fig.add_subplot(111, projection='3d', proj_type='ortho') ax3d.plot(x, y, z, 'ro') # ref points ax3d.plot(xs, ys, zs, 'yo') # spline knots ax3d.plot(x_new, y_new, z_new, 'b--') # spline ax3d.plot(meas_pts[:, 2], meas_pts[:, 1], meas_pts[:, 3], 'g*') # measured points # ax3d.view_init(90, -90) # 2D TOP view # ax3d.view_init(0, -90) # 2D from SOUTH to NORTH view # ax3d.view_init(0, 0) # 2D from EAST to WEST view plt.show() To sum up: I need array contains pairs: [[measured point X, Y, Z], [closest (normal) point on the spline X,Y,Z]]
Given a point P and a line in a 3d space, the distance from the point P and the points of the line is the diagonal of the box, so you wish to minimize this diagonal, the minimum distance will be normal to the line You can use this property. So, for example import numpy as np import pandas as pd import matplotlib.pyplot as plt # generate sample line x = np.linspace(-2, 2, 100) y = np.cbrt( np.exp(2*x) -1 ) z = (y + 1) * (y - 2) # a point P = (-1, 3, 2) # 3d plot fig = plt.figure() ax = fig.add_subplot(111, projection='3d', proj_type='ortho') ax.plot(x, y, z) ax.plot(P[0], P[1], P[2], 'or') plt.show() def distance_3d(x, y, z, x0, y0, z0): """ 3d distance from a point and a line """ dx = x - x0 dy = y - y0 dz = z - z0 d = np.sqrt(dx**2 + dy**2 + dz**2) return d def min_distance(x, y, z, P, precision=5): """ Compute minimum/a distance/s between a point P[x0,y0,z0] and a curve (x,y,z) rounded at `precision`. ARGS: x, y, z (array) P (3dtuple) precision (integer) Returns min indexes and distances array. """ # compute distance d = distance_3d(x, y, z, P[0], P[1], P[2]) d = np.round(d, precision) # find the minima glob_min_idxs = np.argwhere(d==np.min(d)).ravel() return glob_min_idxs, d that gives min_idx, d = min_distance(x, y, z, P) fig = plt.figure() ax = fig.add_subplot(111, projection='3d', proj_type='ortho') ax.plot(x, y, z) ax.plot(P[0], P[1], P[2], 'or') ax.plot(x[min_idx], y[min_idx], z[min_idx], 'ok') for idx in min_idx: ax.plot( [P[0], x[idx]], [P[1], y[idx]], [P[2], z[idx]], 'k--' ) plt.show() print("distance:", d[min_idx]) distance: [2.4721] You can implement a similar function for your needs.
Revolution solid with MatplotLib Python
I'm trying to create a bottle as a revolution solid using MatplotLib. I've got this points: Image of the coordinates Which in terms of coordinates are: coords = [(0.00823433249299356, 0.06230346394288128), (0.04086905251958573, 0.0648935210878489), (0.08386400112604843, 0.0648935210878489), (0.11753474401062763, 0.06541153251684242), (0.14239929260231693, 0.05712334965294601), (0.19109236692770842, 0.05401528107898486), (0.2278711783862488, 0.05142522393401722), (0.24133947554008045, 0.04158300678314021)] The polynomial (more or less accurate) is: Lambda(x, -19493.7965633925*x**6 + 13024.3747084876*x**5 - 3228.16456296349*x**4 + 368.816080918066*x**3 - 20.500262217588*x**2 + 0.545840273670868*x + 0.0590464366057008) Which I get by: # Getting the polynomial: z = np.polyfit(xdata, ydata, 6) # Being xdata and ydata the 2 vector from the coordinates x = sp.symbols('x', real=True) P = sp.Lambda(x,sum((a*x**i for i,a in enumerate(z[::-1])))) print(P) The point describe the outline of the bottle (cast your imagination) being the bottle in the plane XY. How can I get, from that curve, a solid of revolution that recreates a bottle? My objective is to be able to rotate the generator curve and create a solid of revolution, what I've tried is: # Create the polynomial pol = sp.lambdify(x,P(x),"numpy") # Create the matrix of points X = np.linspace(xdata[0], xdata[-1], 50) Y = pol(X) X, Y = np.meshgrid(X, Y) # As long as a bottle is no more than a big amount of small cylinders, my # equation should be more or less like: # Z = x**2 + y** -R**2 # So we create here the equation Z = X**2 + Y**2 - (Y - 0.0115)**2 # We create the #D figure fig = plt.figure() ax = plt.axes(projection="3d") # And we representate it surf = ax.plot_surface(X, Y, Z) # We change the labels ax.set_xlabel('$x$') ax.set_ylabel('$y$') ax.set_zlabel('$z$') # And show the figure plt.show() The problem is that what I get is no longer a bottle (and I think is because how I'm using the plot_surface (I don't get very well how to use it by reading the documentation). What I got is: Image of the plotting. First I thought that was a problem related to the zoom, but I changed it and the figure is the same
I'll reference unutbu's answer to a similar question. import numpy as np import matplotlib.pyplot as plt import mpl_toolkits.mplot3d.axes3d as axes3d fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection='3d') # grab more points between your coordinates, say 100 points u = np.linspace(0.00823433249299356, 0.24133947554008045, 100) def polynomial(x): return -19493.7965633925*x**6 + 13024.3747084876*x**5 - 3228.16456296349*x**4 + 368.816080918066*x**3 - 20.500262217588*x**2 + 0.545840273670868*x + 0.0590464366057008 v = np.linspace(0, 2*np.pi, 60) U, V = np.meshgrid(u, v) X = U Y1 = polynomial(X)*np.cos(V) Z1 = polynomial(X)*np.sin(V) # Revolving around the axis Y2 = 0*np.cos(V) Z2 = 0*np.sin(V) ax.plot_surface(X, Y1, Z1, alpha=0.3, color='red', rstride=6, cstride=12) ax.plot_surface(X, Y2, Z2, alpha=0.3, color='blue', rstride=6, cstride=12) # set the limits of the axes ax.set_xlim3d(-0.3, 0.3) ax.set_ylim3d(-0.3, 0.3) ax.set_zlim3d(-0.3, 0.3) plt.show()
Plot scaled and rotated bivariate distribution using matplotlib
I am trying to plot a bivariate gaussian distribution using matplotlib. I want to do this using the xy coordinates of two scatter points (Group A), (Group B). I want to adjust the distribution by adjusting the COV matrix to account for each Groups velocity and their distance to an additional xy coordinate used as a reference point. I've calculated the distance of each groups xy coordinate to that of the reference point. The distance is expressed as a radius, labelled [GrA_Rad],[GrB_Rad]. So the further they are away from the reference point the greater the radius. I've also calculated velocity labelled [GrA_Vel],[GrB_Vel]. The direction of each group is expressed as the orientation. This is labelled [GrA_Rotation],[GrB_Rotation] Question on how I want the distribution to be adjusted for velocity and distance (radius): I'm hoping to use SVD. Specifically, if I have the rotation angle of each scatter, this provides the direction. The velocity can be used to describe a scaling matrix [GrA_Scaling],[GrB_Scaling]. So this scaling matrix can be used to expand the radius in the x-direction and contract the radius in the y-direction. This expresses the COV matrix. Finally, the distribution mean value is found by translating the groups location (x,y) by half the velocity. Put simply: the radius is applied to each group's scatter point. The COV matrix is adjusted by the radius and velocity. So using the scaling matrix to expand the radius in x-direction and contract in y-direction. The direction is measured from the rotation angle. Then determine the distribution mean value by translating the groups location (x,y) by half the velocity. Below is the df of these variables import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.animation as animation d = ({ 'Time' : [1,2,3,4,5,6,7,8], 'GrA_X' : [10,12,17,16,16,14,12,8], 'GrA_Y' : [10,12,13,7,6,7,8,8], 'GrB_X' : [5,8,13,16,19,15,13,5], 'GrB_Y' : [6,15,12,7,8,9,10,8], 'Reference_X' : [6,8,14,18,13,11,16,15], 'Reference_Y' : [10,12,8,12,15,12,10,8], 'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3], 'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65], 'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0], 'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2], 'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31], 'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4], 'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135], 'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90], }) df = pd.DataFrame(data = d) I've made an animated plot of each xy coordinate. GrA_X = [10,12,17,16,16,14,12,8] GrA_Y = [10,12,13,7,6,7,8,8] GrB_X = [5,8,13,16,19,15,13,5] GrB_Y = [6,15,12,10,8,9,10,8] Item_X = [6,8,14,18,13,11,16,15] Item_Y = [10,12,8,12,15,12,10,8] scatter_GrA = ax.scatter(GrA_X, GrA_Y) scatter_GrB = ax.scatter(GrB_X, GrB_Y) scatter_Item = ax.scatter(Item_X, Item_Y) def animate(i) : scatter_GrA.set_offsets([[GrA_X[0+i], GrA_Y[0+i]]]) scatter_GrB.set_offsets([[GrB_X[0+i], GrB_Y[0+i]]]) scatter_Item.set_offsets([[Item_X[0+i], Item_Y[0+i]]]) ani = animation.FuncAnimation(fig, animate, np.arange(0,9), interval = 1000, blit = False)
Update The question has been updated, and has gotten somewhat clearer. I've updated my code to match. Here's the latest output: Aside from the styling, I think this matches what the OP described. Here's the code that was used to produce the above plot: dfake = ({ 'GrA_X' : [15,15], 'GrA_Y' : [15,15], 'Reference_X' : [15,3], 'Reference_Y' : [15,15], 'GrA_Rad' : [15,25], 'GrA_Vel' : [0,10], 'GrA_Scaling' : [0,0.5], 'GrA_Rotation' : [0,45] }) dffake = pd.DataFrame(dfake) fig,axs = plt.subplots(1, 2, figsize=(16,8)) fig.subplots_adjust(0,0,1,1) plotone(dffake, 'A', 0, xlim=(0,30), ylim=(0,30), fig=fig, ax=axs[0]) plotone(dffake, 'A', 1, xlim=(0,30), ylim=(0,30), fig=fig, ax=axs[1]) plt.show() and the complete implementation of the plotone function that I used is in the code block below. If you just want to know about the math used to generate and transform the 2D gaussian PDF, check out the mvpdf function (and the rot and getcov functions it depends on): import numpy as np import pandas as pd import matplotlib.pyplot as plt import scipy.stats as sts def rot(theta): theta = np.deg2rad(theta) return np.array([ [np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)] ]) def getcov(radius=1, scale=1, theta=0): cov = np.array([ [radius*(scale + 1), 0], [0, radius/(scale + 1)] ]) r = rot(theta) return r # cov # r.T def mvpdf(x, y, xlim, ylim, radius=1, velocity=0, scale=0, theta=0): """Creates a grid of data that represents the PDF of a multivariate gaussian. x, y: The center of the returned PDF (xy)lim: The extent of the returned PDF radius: The PDF will be dilated by this factor scale: The PDF be stretched by a factor of (scale + 1) in the x direction, and squashed by a factor of 1/(scale + 1) in the y direction theta: The PDF will be rotated by this many degrees returns: X, Y, PDF. X and Y hold the coordinates of the PDF. """ # create the coordinate grids X,Y = np.meshgrid(np.linspace(*xlim), np.linspace(*ylim)) # stack them into the format expected by the multivariate pdf XY = np.stack([X, Y], 2) # displace xy by half the velocity x,y = rot(theta) # (velocity/2, 0) + (x, y) # get the covariance matrix with the appropriate transforms cov = getcov(radius=radius, scale=scale, theta=theta) # generate the data grid that represents the PDF PDF = sts.multivariate_normal([x, y], cov).pdf(XY) return X, Y, PDF def plotmv(x, y, xlim=None, ylim=None, radius=1, velocity=0, scale=0, theta=0, xref=None, yref=None, fig=None, ax=None): """Plot an xy point with an appropriately tranformed 2D gaussian around it. Also plots other related data like the reference point. """ if xlim is None: xlim = (x - 5, x + 5) if ylim is None: ylim = (y - 5, y + 5) if fig is None: fig = plt.figure(figsize=(8,8)) ax = fig.gca() elif ax is None: ax = fig.gca() # plot the xy point ax.plot(x, y, '.', c='C0', ms=20) if not (xref is None or yref is None): # plot the reference point, if supplied ax.plot(xref, yref, '.', c='w', ms=12) # plot the arrow leading from the xy point if velocity > 0: ax.arrow(x, y, *rot(theta) # (velocity, 0), width=.4, length_includes_head=True, ec='C0', fc='C0') # fetch the PDF of the 2D gaussian X, Y, PDF = mvpdf(x, y, xlim=xlim, ylim=ylim, radius=radius, velocity=velocity, scale=scale, theta=theta) # normalize PDF by shifting and scaling, so that the smallest value is 0 and the largest is 1 normPDF = PDF - PDF.min() normPDF = normPDF/normPDF.max() # plot and label the contour lines of the 2D gaussian cs = ax.contour(X, Y, normPDF, levels=6, colors='w', alpha=.5) ax.clabel(cs, fmt='%.3f', fontsize=12) # plot the filled contours of the 2D gaussian. Set levels high for smooth contours cfs = ax.contourf(X, Y, normPDF, levels=50, cmap='viridis', vmin=-.9, vmax=1) # create the colorbar and ensure that it goes from 0 -> 1 cbar = fig.colorbar(cfs, ax=ax) cbar.set_ticks([0, .2, .4, .6, .8, 1]) # add some labels ax.grid() ax.set_xlabel('X distance (M)') ax.set_ylabel('Y distance (M)') # ensure that x vs y scaling doesn't disrupt the transforms applied to the 2D gaussian ax.set_aspect('equal', 'box') return fig, ax def fetchone(df, l, i, **kwargs): """Fetch all the needed data for one xy point """ keytups = ( ('x', 'Gr%s_X'%l), ('y', 'Gr%s_Y'%l), ('radius', 'Gr%s_Rad'%l), ('velocity', 'Gr%s_Vel'%l), ('scale', 'Gr%s_Scaling'%l), ('theta', 'Gr%s_Rotation'%l), ('xref', 'Reference_X'), ('yref', 'Reference_Y') ) ret = {k:df.loc[i, l] for k,l in keytups} # add in any overrides ret.update(kwargs) return ret def plotone(df, l, i, xlim=None, ylim=None, fig=None, ax=None, **kwargs): """Plot exactly one point from the dataset """ # look up all the data to plot one datapoint xydata = fetchone(df, l, i, **kwargs) # do the plot return plotmv(xlim=xlim, ylim=ylim, fig=fig, ax=ax, **xydata) Old answer -2 I've adjusted my answer to match the example the OP posted: Here's the code that produced the above image: import numpy as np import pandas as pd import matplotlib.pyplot as plt import scipy.stats as sts def rot(theta): theta = np.deg2rad(theta) return np.array([ [np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)] ]) def getcov(radius=1, scale=1, theta=0): cov = np.array([ [radius*(scale + 1), 0], [0, radius/(scale + 1)] ]) r = rot(theta) return r # cov # r.T def datalimits(*data, pad=.15): dmin,dmax = min(d.min() for d in data), max(d.max() for d in data) spad = pad*(dmax - dmin) return dmin - spad, dmax + spad d = ({ 'Time' : [1,2,3,4,5,6,7,8], 'GrA_X' : [10,12,17,16,16,14,12,8], 'GrA_Y' : [10,12,13,7,6,7,8,8], 'GrB_X' : [5,8,13,16,19,15,13,5], 'GrB_Y' : [6,15,12,7,8,9,10,8], 'Reference_X' : [6,8,14,18,13,11,16,15], 'Reference_Y' : [10,12,8,12,15,12,10,8], 'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3], 'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65], 'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0], 'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2], 'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31], 'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4], 'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135], 'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90], }) df = pd.DataFrame(data=d) limitpad = .5 clevels = 5 cflevels = 50 xmin,xmax = datalimits(df['GrA_X'], df['GrB_X'], pad=limitpad) ymin,ymax = datalimits(df['GrA_Y'], df['GrB_Y'], pad=limitpad) X,Y = np.meshgrid(np.linspace(xmin, xmax), np.linspace(ymin, ymax)) fig = plt.figure(figsize=(10,6)) ax = plt.gca() Zs = [] for l,color in zip('AB', ('red', 'yellow')): # plot all of the points from a single group ax.plot(df['Gr%s_X'%l], df['Gr%s_Y'%l], '.', c=color, ms=15, label=l) Zrows = [] for _,row in df.iterrows(): x,y = row['Gr%s_X'%l], row['Gr%s_Y'%l] cov = getcov(radius=row['Gr%s_Rad'%l], scale=row['Gr%s_Scaling'%l], theta=row['Gr%s_Rotation'%l]) mnorm = sts.multivariate_normal([x, y], cov) Z = mnorm.pdf(np.stack([X, Y], 2)) Zrows.append(Z) Zs.append(np.sum(Zrows, axis=0)) # plot the reference points # create Z from the difference of the sums of the 2D Gaussians from group A and group B Z = Zs[0] - Zs[1] # normalize Z by shifting and scaling, so that the smallest value is 0 and the largest is 1 normZ = Z - Z.min() normZ = normZ/normZ.max() # plot and label the contour lines cs = ax.contour(X, Y, normZ, levels=clevels, colors='w', alpha=.5) ax.clabel(cs, fmt='%2.1f', colors='w')#, fontsize=14) # plot the filled contours. Set levels high for smooth contours cfs = ax.contourf(X, Y, normZ, levels=cflevels, cmap='viridis', vmin=0, vmax=1) # create the colorbar and ensure that it goes from 0 -> 1 cbar = fig.colorbar(cfs, ax=ax) cbar.set_ticks([0, .2, .4, .6, .8, 1]) ax.set_aspect('equal', 'box') Old answer -1 It's a little hard to tell exactly what you're after. It is possible to scale and rotate a multivariate gaussian distribution via its covariance matrix. Here's an example of how to do so based on your data: import numpy as np import pandas as pd import matplotlib.pyplot as plt import scipy.stats as sts def rot(theta): theta = np.deg2rad(theta) return np.array([ [np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)] ]) def getcov(scale, theta): cov = np.array([ [1*(scale + 1), 0], [0, 1/(scale + 1)] ]) r = rot(theta) return r # cov # r.T d = ({ 'Time' : [1,2,3,4,5,6,7,8], 'GrA_X' : [10,12,17,16,16,14,12,8], 'GrA_Y' : [10,12,13,7,6,7,8,8], 'GrB_X' : [5,8,13,16,19,15,13,5], 'GrB_Y' : [6,15,12,7,8,9,10,8], 'Reference_X' : [6,8,14,18,13,11,16,15], 'Reference_Y' : [10,12,8,12,15,12,10,8], 'GrA_Rad' : [8.3,8.25,8.2,8,8.15,8.15,8.2,8.3], 'GrB_Rad' : [8.3,8.25,8.3,8.4,8.6,8.4,8.3,8.65], 'GrA_Vel' : [0,2.8,5.1,6.1,1.0,2.2,2.2,4.0], 'GrB_Vel' : [0,9.5,5.8,5.8,3.16,4.12,2.2,8.2], 'GrA_Scaling' : [0,0.22,0.39,0.47,0.07,0.17,0.17,0.31], 'GrB_Scaling' : [0,0.53,0.2,0.2,0.06,0.1,0.03,0.4], 'GrA_Rotation' : [0,45,23.2,-26.56,-33.69,-36.86,-45,-135], 'GrB_Rotation' : [0,71.6,36.87,5.2,8.13,16.70,26.57,90], }) df = pd.DataFrame(data=d) xmin,xmax = min(df['GrA_X'].min(), df['GrB_X'].min()), max(df['GrA_X'].max(), df['GrB_X'].max()) ymin,ymax = min(df['GrA_Y'].min(), df['GrB_Y'].min()), max(df['GrA_Y'].max(), df['GrB_Y'].max()) X,Y = np.meshgrid( np.linspace(xmin - (xmax - xmin)*.1, xmax + (xmax - xmin)*.1), np.linspace(ymin - (ymax - ymin)*.1, ymax + (ymax - ymin)*.1) ) fig,axs = plt.subplots(df.shape[0], sharex=True, figsize=(4, 4*df.shape[0])) fig.subplots_adjust(0,0,1,1,0,-.82) for (_,row),ax in zip(df.iterrows(), axs): for c in 'AB': x,y = row['Gr%s_X'%c], row['Gr%s_Y'%c] cov = getcov(scale=row['Gr%s_Scaling'%c], theta=row['Gr%s_Rotation'%c]) mnorm = sts.multivariate_normal([x, y], cov) Z = mnorm.pdf(np.stack([X, Y], 2)) ax.contour(X, Y, Z) ax.plot(row['Gr%s_X'%c], row['Gr%s_Y'%c], 'x') ax.set_aspect('equal', 'box') This outputs: