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:
Related
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...
I have a 1D distribution (x values vs probability, shown below) and I would like to convert that to a 2D plot like the one shown below in which the color gradient is based on the values probabilities.
Currently, my code just plot in a qualitative manner because I am manually defining the array v1 and the color list. I tried my best to crack this and understand how to do it, but I failed. Does anyone have a suggestion?
def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs):
"""
Draw a gradient image based on a colormap.
Parameters
----------
ax : Axes
The axes to draw on.
extent
The extent of the image as (xmin, xmax, ymin, ymax).
By default, this is in Axes coordinates but may be
changed using the *transform* keyword argument.
direction : float
The direction of the gradient. This is a number in
range 0 (=vertical) to 1 (=horizontal).
cmap_range : float, float
The fraction (cmin, cmax) of the colormap that should be
used for the gradient, where the complete colormap is (0, 1).
**kwargs
Other parameters are passed on to `.Axes.imshow()`.
In particular useful is *cmap*.
"""
phi = direction * np.pi / 2
v = np.array([np.cos(phi), np.sin(phi)])
X = np.array([[v # [1, 0], v # [1, 1]],
[v # [0, 0], v # [0, 1]]])
a, b = cmap_range
X = a + (b - a) / X.max() * X
im = ax.imshow(X, extent=extent, interpolation='bicubic',
vmin=0, vmax=1, **kwargs)
return im
v1 = [0, 0.15, 0.5, 0.85, 1.0] # | Those two lines here
b = ["white","lightblue", "dodgerblue","lightblue", "white"] # | were the best I could do
bl = list(zip(v1,b))
blue_grad=LinearSegmentedColormap.from_list('custom',bl, N=256)
xmin, xmax = xlim = 0, 4
ymin, ymax = ylim = -300, 300
fig, ax = plt.subplots()
ax.set(xlim=xlim, ylim=ylim, autoscale_on=False)
gradient_image(ax, direction=1, extent=(0 , 2, -300, 300), cmap=blue_grad, cmap_range=(0., 1), alpha=0.5)
Here is a minimal example with a gaussian distribution (code for generating the gaussian distribution was adapted from this):
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats
mu=0 #Create gaussian distribution
sigma=1
x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
y=stats.norm.pdf(x, mu, sigma)
y_expand=np.expand_dims(y, axis=0) #expanding dimensions to use imshow
plt.plot(x, stats.norm.pdf(x, mu, sigma),color='k',lw=3,ls='--')# plot distribution
extent = np.min(x), np.max(x), np.min(y), np.max(y)
plt.imshow(y_expand,interpolation=None,aspect='auto',cmap='Blues',extent=extent) #plot imshow
plt.colorbar()
plt.show()
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.
I am trying to plot a multivariate distribution that is produced from multiple xy coordinates.
The code below aims to get each coordinate and apply it with a radius ([_Rad]). The COV matrix is then adjusted by scaling factor ([_Scaling]) to expand the radius in x-direction and contract in y-direction. The direction of this is measured by the rotation angle ([_Rotation]).
The output is expressed as a probability function, which represents the influence of each groups coordinates over a certain space.
Although, at present I can only get the code to apply this to the last set of coordinates in the df. So using the input below, only A3_X, A3_Y is working. A1_X, A1_Y, A2_X, A2_Y and B1_X, B1_Y, B2_X, B2_Y. Please see attached figure for a visual representation.
Note: Apologies for the long df. It was the only way to replicate my dataset.
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.values.min() for d in data), max(d.values.max() for d in data)
spad = pad*(dmax - dmin)
return dmin - spad, dmax + spad
d = ({
'Time' : [1],
'A1_Y' : [5883.102906],
'A1_X' : [3321.527705],
'A2_Y' : [5898.467202],
'A2_X' : [3328.331657],
'A3_Y' : [5886.270552],
'A3_X' : [3366.777169],
'B1_Y' : [5897.925245],
'B1_X' : [3297.143092],
'B2_Y' : [5905.137781],
'B2_X' : [3321.167842],
'B3_Y' : [5888.291025],
'B3_X' : [3347.263205],
'A1_Radius' : [10.3375199],
'A2_Radius' : [10.0171423],
'A3_Radius' : [11.42129333],
'B1_Radius' : [18.69514267],
'B2_Radius' : [10.65877044],
'B3_Radius' : [9.947025444],
'A1_Scaling' : [0.0716513620],
'A2_Scaling' : [0.0056262380],
'A3_Scaling' : [0.0677243260,],
'B1_Scaling' : [0.0364290850],
'B2_Scaling' : [0.0585827450],
'B3_Scaling' : [0.0432806750],
'A1_Rotation' : [20.58078926],
'A2_Rotation' : [173.5056346],
'A3_Rotation' : [36.23648405],
'B1_Rotation' : [79.81849817],
'B2_Rotation' : [132.2437404],
'B3_Rotation' : [44.28198078],
})
df = pd.DataFrame(data=d)
A_Y = df[df.columns[1::2][:3]]
A_X = df[df.columns[2::2][:3]]
B_Y = df[df.columns[7::2][:3]]
B_X = df[df.columns[8::2][:3]]
A_Radius = df[df.columns[13:16]]
B_Radius = df[df.columns[16:19]]
A_Scaling = df[df.columns[19:22]]
B_Scaling = df[df.columns[22:25]]
A_Rotation = df[df.columns[25:28]]
B_Rotation = df[df.columns[28:31]]
limitpad = .5
clevels = 5
cflevels = 50
xmin,xmax = datalimits(A_X, B_X, pad=limitpad)
ymin,ymax = datalimits(A_Y, B_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', 'blue')):
ax.plot(A_X.iloc[0], A_Y.iloc[0], '.', c='red', ms=10, label=l, alpha = 0.6)
ax.plot(B_X.iloc[0], B_Y.iloc[0], '.', c='blue', ms=10, label=l, alpha = 0.6)
Zrows = []
for _,row in df.iterrows():
for i in [1,2,3]:
x,y = row['{}{}_X'.format(l,i)], row['{}{}_Y'.format(l,i)]
cov = getcov(radius=row['{}{}_Radius'.format(l,i)],scale=row['{}{}_Scaling'.format(l,i)], theta=row['{}{}_Rotation'.format(l,i)])
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))
Z = Zs[0] - Zs[1]
normZ = Z - Z.min()
normZ = normZ/normZ.max()
cs = ax.contour(X, Y, normZ, levels=clevels, colors='w', alpha=.5)
ax.clabel(cs, fmt='%2.1f', colors='w')#, fontsize=14)
cfs = ax.contourf(X, Y, normZ, levels=cflevels, cmap='viridis', vmin=0, vmax=1)
cbar = fig.colorbar(cfs, ax=ax)
cbar.set_ticks([0, .2, .4, .6, .8, 1])
As you can see below. The code is only applying to A3_X, A3_Y and B3_X, B3_Y.
It's not applying to coordinates A1_X, A1_Y, A2_X, A2_Y and B1_X, B1_Y, B2_X, B2_Y.
There's an error in the way that you're iterating over the point data. The way that you have your dataframe organized makes it hard to figure out the appropriate way to iterate over the data, and makes it easy to run into errors of the kind you're getting. It would be better if your df was organized such that you could easily iterate over the subsets of your data representing each group A and B at each time. If you split out the times from your data dictionary d, here's how you can construct an easier to work with df:
import pandas as pd
time = [1]
d = ({
'A1_Y' : [5883.102906],
'A1_X' : [3321.527705],
'A2_Y' : [5898.467202],
'A2_X' : [3328.331657],
'A3_Y' : [5886.270552],
'A3_X' : [3366.777169],
'B1_Y' : [5897.925245],
'B1_X' : [3297.143092],
'B2_Y' : [5905.137781],
'B2_X' : [3321.167842],
'B3_Y' : [5888.291025],
'B3_X' : [3347.263205],
'A1_Radius' : [10.3375199],
'A2_Radius' : [10.0171423],
'A3_Radius' : [11.42129333],
'B1_Radius' : [18.69514267],
'B2_Radius' : [10.65877044],
'B3_Radius' : [9.947025444],
'A1_Scaling' : [0.0716513620],
'A2_Scaling' : [0.0056262380],
'A3_Scaling' : [0.0677243260,],
'B1_Scaling' : [0.0364290850],
'B2_Scaling' : [0.0585827450],
'B3_Scaling' : [0.0432806750],
'A1_Rotation' : [20.58078926],
'A2_Rotation' : [173.5056346],
'A3_Rotation' : [36.23648405],
'B1_Rotation' : [79.81849817],
'B2_Rotation' : [132.2437404],
'B3_Rotation' : [44.28198078],
})
# a list of tuples of the form ((time, group_id, point_id, value_label), value)
tuples = [((t, k.split('_')[0][0], int(k.split('_')[0][1]), k.split('_')[1]), v[i]) for k,v in d.items() for i,t in enumerate(time)]
df = pd.Series(dict(tuples)).unstack(-1)
df.index.names = ['time', 'group', 'id']
print(df)
Output:
Radius Rotation Scaling X Y
time group id
1 A 1 10.337520 20.580789 0.071651 3321.527705 5883.102906
2 10.017142 173.505635 0.005626 3328.331657 5898.467202
3 11.421293 36.236484 0.067724 3366.777169 5886.270552
B 1 18.695143 79.818498 0.036429 3297.143092 5897.925245
2 10.658770 132.243740 0.058583 3321.167842 5905.137781
3 9.947025 44.281981 0.043281 3347.263205 5888.291025
This will make it much easier to iterate over the subsets in your data. Here's how you would iterate over the sub-dataframes for each group at each timepoint:
for time, tdf in df.groupby('time'):
for group, gdf in tdf.groupby('group'):
...
Here's an updated version of my code from your previous question that uses this better-organized dataframe to create the plot you want at every time point:
for time,subdf in df.groupby('time'):
plotmvs(subdf)
Output:
Here's the complete code of the above plotmvs function:
import numpy as np
import pandas as pd
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.pyplot as plt
import scipy.stats as sts
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
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 mvpdfs(xs, ys, xlim, ylim, radius=None, velocity=None, scale=None, theta=None):
PDFs = []
for i,(x,y) in enumerate(zip(xs,ys)):
kwargs = {
'radius': radius[i] if radius is not None else 1,
'velocity': velocity[i] if velocity is not None else 0,
'scale': scale[i] if scale is not None else 0,
'theta': theta[i] if theta is not None else 0,
'xlim': xlim,
'ylim': ylim
}
X, Y, PDF = mvpdf(x, y, **kwargs)
PDFs.append(PDF)
return X, Y, np.sum(PDFs, axis=0)
def plotmvs(df, xlim=None, ylim=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 = datalimits(df['X'])
if ylim is None: ylim = datalimits(df['Y'])
if fig is None:
fig = plt.figure(figsize=(8,8))
ax = fig.gca()
elif ax is None:
ax = fig.gca()
PDFs = []
for (group,gdf),color in zip(df.groupby('group'), ('red', 'blue')):
# plot the xy points of each group
ax.plot(*gdf[['X','Y']].values.T, '.', c=color)
# fetch the PDFs of the 2D gaussian for each group
kwargs = {
'radius': gdf['Radius'].values if 'Radius' in gdf else None,
'velocity': gdf['Velocity'].values if 'Velocity' in gdf else None,
'scale': gdf['Scaling'].values if 'Scaling' in gdf else None,
'theta': gdf['Rotation'].values if 'Rotation' in gdf else None,
'xlim': xlim,
'ylim': ylim
}
X, Y, PDF = mvpdfs(gdf['X'].values, gdf['Y'].values, **kwargs)
PDFs.append(PDF)
# create the PDF for all points from the difference of the sums of the 2D Gaussians from group A and group B
PDF = PDFs[0] - PDFs[1]
# 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')
# create the colorbar and ensure that it goes from 0 -> 1
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1)
cbar = fig.colorbar(cfs, ax=ax, cax=cax)
cbar.set_ticks([0, .2, .4, .6, .8, 1])
# ensure that x vs y scaling doesn't disrupt the transforms applied to the 2D gaussian
ax.set_aspect('equal', 'box')
return fig, ax
Simply adjust indentation especially with middle inner nested for loop and reset Zrows list when iterating across data frame rows. See comments in code for specific changes:
...
for _, row in df.iterrows():
# MOVE ZROWS INSIDE
Zrows = []
for i in [1,2,3]:
x,y = row['{}{}_X'.format(l,i)], row['{}{}_Y'.format(l,i)]
# INDENT cov AND LATER CALCS TO RUN ACROSS ALL 1,2,3
cov = getcov(radius=row['{}{}_Radius'.format(l,i)],
scale=row['{}{}_Scaling'.format(l,i)],
theta=row['{}{}_Rotation'.format(l,i)])
mnorm = sts.multivariate_normal([x, y], cov)
Z = mnorm.pdf(np.stack([X, Y], 2))
# APPEND TO BE CLEANED OUT WITH EACH ROW
Zrows.append(Z)
Zs.append(np.sum(Zrows, axis=0))
...
There is a lot going on in this code. A small thing I noticed was that it looks like you are not using the df.columns indexing correctly. If you look at A_Y the output is:
A1_Rotation A1_X A2_Radius
0 20.580789 3321.527705 10.017142
I think you are mixing columns. Maybe use df[['A1_Y', 'A2_Y', 'A3_Y']] to get the exact columns or just put all the A_Y values into a single column.
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)