I want to rotate a 5-atom crystal defined by X,Y,Z coordinates of atoms by a random angle. My initial idea was to use an external package to generate a rotation matrix (https://github.com/qobilidop/randrot) and then multiplying this matrix by a vector, which defines the coordinates of a single atom. However, that did not work at all and all the atoms got dispersed. Here's a function I wrote for that purpose:
def rotation():
crystal = []
rotmat = np.asarray(randrot.generate(3)) #generates 3x3 rotation matrix
for x,y,z in zip(new_x, new_y, new_z):
vec = np.array([x,y,z])
rot = vec.dot(rotmat)
for elem in rot:
crystal.append(elem)
return np.array(crystal).reshape([5,3])
rotated = rotation()
ax.scatter(rotated[0], rotated[1], rotated[2], marker='.', s=100, color='green')
Here's how it looks (red is the initial placement, green is after rotation):
pyplot
Here is an example code that rotates given 3d points about a randomly generated rotation matrix, rotation matrix creation is taken from another answer.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import math
# taken from https://stackoverflow.com/questions/6802577/rotation-of-3d-vector
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]])
# initial xyz coordinates
xs = [0, 1, 1, 1, 1, -1, -1, -1, -1]
ys = [0, 1, 1, -1, -1, 1, 1, -1, -1]
zs = [0, 1, -1, 1, -1, 1, -1, 1, -1]
atoms_initial = np.array([xs, ys, zs]).T
# specify rotation matrix parameters
# let us generate a random axis and angle for rotation
rotation_axis = np.random.uniform(low=0, high=1, size=3) # three numbers between 0 and 1
rotation_angle = np.random.uniform(low=0, high=2*np.pi, size=1) # random number between 0 and 2pi
print("Rotation axis:{}, rotation angle:{} radians".format(rotation_axis, rotation_angle))
# create our rotation matrix
rotmat = rotation_matrix(rotation_axis, rotation_angle)
# apply rotation matrix to our points
atoms_rotated = np.dot(atoms_initial, rotmat)
# draw
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(atoms_initial[:,0], atoms_initial[:,1], atoms_initial[:,2], marker='.', s=100, color='red')
ax.scatter(atoms_rotated[:,0], atoms_rotated[:,1], atoms_rotated[:,2], marker='.', s=100, color="green")
plt.show()
Related
I need help to create a torus out of a circle by revolving it about x=2r, r is the radius of the circle.
I am open to either JULIA code or Python code. Whichever that can solve my problem the most efficient.
I have Julia code to plot circle and the x=2r as the axis of revolution.
using Plots, LaTeXStrings, Plots.PlotMeasures
gr()
θ = 0:0.1:2.1π
x = 0 .+ 2cos.(θ)
y = 0 .+ 2sin.(θ)
plot(x, y, label=L"x^{2} + y^{2} = a^{2}",
framestyle=:zerolines, legend=:outertop)
plot!([4], seriestype="vline", color=:green, label="x=2a")
I want to create a torus out of it, but unable, meanwhile I have solid of revolution Python code like this:
# Calculate the surface area of y = sqrt(r^2 - x^2)
# revolved about the x-axis
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
x = sy.Symbol("x", nonnegative=True)
r = sy.Symbol("r", nonnegative=True)
def f(x):
return sy.sqrt(r**2 - x**2)
def fd(x):
return sy.simplify(sy.diff(f(x), x))
def f2(x):
return sy.sqrt((1 + (fd(x)**2)))
def vx(x):
return 2*sy.pi*(f(x)*sy.sqrt(1 + (fd(x) ** 2)))
vxi = sy.Integral(vx(x), (x, -r, r))
vxf = vxi.simplify().doit()
vxn = vxf.evalf()
n = 100
fig = plt.figure(figsize=(14, 7))
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222, projection='3d')
ax3 = fig.add_subplot(223)
ax4 = fig.add_subplot(224, projection='3d')
# 1 is the starting point. The first 3 is the end point.
# The last 200 is the number of discretization points.
# help(np.linspace) to read its documentation.
x = np.linspace(1, 3, 200)
# Plot the circle
y = np.sqrt(2 ** 2 - x ** 2)
t = np.linspace(0, np.pi * 2, n)
xn = np.outer(x, np.cos(t))
yn = np.outer(x, np.sin(t))
zn = np.zeros_like(xn)
for i in range(len(x)):
zn[i:i + 1, :] = np.full_like(zn[0, :], y[i])
ax1.plot(x, y)
ax1.set_title("$f(x)$")
ax2.plot_surface(xn, yn, zn)
ax2.set_title("$f(x)$: Revolution around $y$")
# find the inverse of the function
y_inverse = x
x_inverse = np.power(2 ** 2 - y_inverse ** 2, 1 / 2)
xn_inverse = np.outer(x_inverse, np.cos(t))
yn_inverse = np.outer(x_inverse, np.sin(t))
zn_inverse = np.zeros_like(xn_inverse)
for i in range(len(x_inverse)):
zn_inverse[i:i + 1, :] = np.full_like(zn_inverse[0, :], y_inverse[i])
ax3.plot(x_inverse, y_inverse)
ax3.set_title("Inverse of $f(x)$")
ax4.plot_surface(xn_inverse, yn_inverse, zn_inverse)
ax4.set_title("$f(x)$: Revolution around $x$ \n Surface Area = {}".format(vxn))
plt.tight_layout()
plt.show()
Here is a way that actually allows rotating any figure in the XY plane around the Y axis.
"""
Rotation of a figure in the XY plane about the Y axis:
ϕ = angle of rotation
z' = z * cos(ϕ) - x * sin(ϕ)
x' = z * sin(ϕ) + x * cos(ϕ)
y' = y
"""
using Plots
# OP definition of the circle, but we put center at x, y of 4, 0
# for the torus, otherwise we get a bit of a sphere
θ = 0:0.1:2.1π
x = 4 .+ 2cos.(θ) # center at (s, 0, 0)
y = 0 .+ 2sin.(θ)
# add the original z values as 0
z = zeros(length(x))
plot(x, y, z, color=:red)
# add the rotation axis
ϕ = 0:0.1:π/2 # for full torus use 2π at stop of range
xprime, yprime, zprime = Float64[], Float64[], Float64[]
for a in ϕ, i in eachindex(θ)
push!(zprime, z[i] + z[i] * cos(a) - x[i] * sin(a))
push!(xprime, z[i] * sin(a) + x[i] * cos(a))
push!(yprime, y[i])
end
plot!(xprime, yprime, zprime, alpha=0.3, color=:green)
Here is a way using the Meshes package for the construction of the mesh and the MeshViz package for the visualization. You'll just have to translate to fulfill your desiderata.
using Meshes
using MeshViz
using LinearAlgebra
using GLMakie
# revolution of the polygon defined by (x,y) around the z-axis
# x and y have the same length
function revolution(x, y, n)
u_ = LinRange(0, 2*pi, n+1)[1:n]
j_ = 1:(length(x) - 1) # subtract 1 because of periodicity
function f(u, j)
return [x[j] * sin(u), x[j] * cos(u), y[j]]
end
points = [f(u, j) for u in u_ for j in j_]
topo = GridTopology((length(j_), n), (true, true))
return SimpleMesh(Meshes.Point.(points), topo)
end
# define the section to be rotated: a circle
R = 3 # major radius
r = 1 # minor radius
ntheta = 100
theta_ = LinRange(0, 2*pi, ntheta)
x = [R + r*cos(theta) for theta in theta_]
y = [r*sin(theta) for theta in theta_]
# make mesh
mesh = revolution(x, y, 100)
# visualize mesh
viz(mesh)
EDIT: animation
using Meshes
using MeshViz
using LinearAlgebra
using GLMakie
using Makie
using Printf
function revolutionTorus(R, r, alpha; n1=30, n2=90)
theta_ = LinRange(0, 2, n1+1)[1:n1]
x = [R + r*cospi(theta) for theta in theta_]
y = [r*sinpi(theta) for theta in theta_]
full = alpha == 2
u_ = LinRange(0, alpha, n2 + full)[1:n2]
function f(u, j)
return [x[j] * sinpi(u), x[j] * cospi(u), y[j]]
end
points = [f(u, j) for u in u_ for j in 1:n1]
topo = GridTopology((n1, n2 - !full), (true, full))
return SimpleMesh(Meshes.Point.(points), topo)
end
# generates `nframes` meshes for alpha = 0 -> 2 (alpha is a multiple of pi)
R = 3
r = 1
nframes = 10
alpha_ = LinRange(0, 2, nframes+1)[2:(nframes+1)]
meshes = [revolutionTorus(R, r, alpha) for alpha in alpha_]
# draw and save the frames in a loop
for i in 1:nframes
# make a bounding box in order that all frames have the same aspect
fig, ax, plt =
viz(Meshes.Box(Meshes.Point(-4.5, -4.5, -2.5), Meshes.Point(4.5, 4.5, 2.5)); alpha = 0)
ax.show_axis = false
viz!(meshes[i])
scale!(ax.scene, 1.8, 1.8, 1.8)
png = #sprintf "revolutionTorus%02d.png" i
Makie.save(png, fig)
end
# make GIF with ImageMagick
comm = #cmd "convert -delay 1x2 'revolutionTorus*.png' revolutionTorus.gif"
run(comm)
Context: Convert an .iges to .vtk.
I have the following equation Ax^2+Bxy+Cy^2+Dx+Ey+F=0 representing a conic section.
The parameters A~F are given. I want to find points on the conic section, so that I can connect them with lines, and make a mesh.
The reason I need the points instead of just using matplotlib Ellipse is because I'm creating a mesh not a plot.
It is in 3 dimension space, but I first get points on xy plane, and use affine transformation to send it to 3 dim.
Question: How do I find points given an implicit equation?
To avoid spending too much time on this, I wrote some code that seems to handle general ellipses. It can be expanded for other conics, depending on what is needed.
The code takes in the coefficients of a general quadratic equation of an ellipse and a number of desired points to be generated on the ellipse and generates a set of points on the ellipse.
import numpy as np
def equation(conic, points):
'''
equation of a conic with coefficients 'conic'
applied to a matrix number_of_points x 3 whose each row is the coordinates
of each point
'''
c = np.array(conic)
x = np.array([points[:,0]**2, points[:, 0]*points[:,1], points[:,1]**2, points[:,0], points[:,1], np.ones(points.shape[0])])
return c.dot(x)
def equation_to_matrix(eq):
'''
eq[0]*x**2 + eq[1]*x*y + eq[2]*y**2 + eq[3]*x + eq[4]*y + eq[5] = 0
'''
return np.array([[2*eq[0], eq[1], eq[3]],
[ eq[1], 2*eq[2], eq[4]],
[ eq[3], eq[4], 2*eq[5]]]) / 2
def solve_quadratic(a, b, c):
'''
solves
ax^2 + bx + c = 0
'''
D = b**2 - 4*a*c
D = np.sqrt(D)
return (-b-D)/(2*a), (-b+D)/(2*a)
def eigen2(S):
'''
solves the eigen-decomposition problem
for a 2x2 symmetric matrix
'''
k1, k2 = solve_quadratic(1, -S[0,0]-S[1,1], S[0,0]*S[1,1] - S[0,1]*S[1,0])
u1 = np.array([-S[0,1], S[0,0]-k1, 0])
u1 = u1 / np.sqrt(u1.dot(u1))
u2 = np.array([-u1[1], u1[0], 0])
return np.array([k1, k2]), np.array([u1, u2, np.array([0,0,1])]).T
def center(conic_matrix):
center = np.linalg.solve(conic_matrix, np.array([0,0,1]))
return center/center[2]
def find_rotation_and_translation(conic_matrix):
'''
conic = c[0]x^2 + c[1]*xy + c[2]*y^2 + c[3]*x + c[4]*y + c[5] = 0
the result is rotation U such that U.T C U = diag
'''
k, U = eigen2(conic_matrix)
U[:,2] = center(conic_matrix)
return U, k
def find_transform(conic):
C = equation_to_matrix(conic)
U, k = find_rotation_and_translation(C)
C = (U.T).dot(C.dot(U))
C = - C / C[2,2]
k = np.array([1/np.sqrt(C[0,0]), 1/np.sqrt(C[1,1]), 1])
return U.dot(np.diag(k))
def generate_points_on(conic, num_points):
'''
conic = [c[0], c[1], c[2], c[3], c[4], c[5]]
coefficients of the qudaratic equation:
conic: c[0]x^2 + c[1]*xy + c[2]*y^2 + c[3]*x + c[4]*y + c[5] = 0
result is the affine transformation (scaling, rotation, translation)
that maps the unit circle to the ellipse defined by the coefficients
'conic'
'''
cos_ = np.cos(2*np.pi* np.arange(0, num_points)/ num_points)
sin_ = np.sin(2*np.pi* np.arange(0, num_points)/ num_points)
U = find_transform(conic)
points = np.array([cos_, sin_, np.ones(num_points)])
return ((U.dot(points)).T)[:,[0,1]]
'''
Test:
'''
'''
Ellipse with equation whose coefficients are in the list E.
The ellipse has semi-major axes 2 and 1,
it is rotated 60 deg from the horizontal,
and its center is at (1, 4)
'''
E = [ 3.25, -2.59807621, 1.75, -23.40192379, 6.89230485, 39.35769515]
'''
U maps points from unit circle to points on E
'''
U = find_transform(E)
print(U)
'''
the set of points on the ellipse E
'''
p = generate_points_on(E, num_points = 20)
print(p)
'''
check that the points p lie on the ellipse E
'''
print(equation(E, p).round(10))
'''
plot
'''
fig = plt.figure()
ax = fig.add_subplot()
ax.plot(p[:,0], p[:,1], 'ro')
ax.set_aspect('equal')
plt.show()
The code below handles the case of a hyperbola. It largely adapts the code from here
import numpy as np
import matplotlib.pyplot as plt
def equation_to_matrix(eq):
'''
eq[0]*x**2 + eq[1]*x*y + eq[2]*y**2 + eq[3]*x + eq[4]*y + eq[5] = 0
'''
return np.array([[2*eq[0], eq[1], eq[3]],
[ eq[1], 2*eq[2], eq[4]],
[ eq[3], eq[4], 2*eq[5]]]) / 2
def hyp_params_from_general(coeffs):
# get the matrix of the quadratic equation
Aq = equation_to_matrix(coeffs)
# get the matrix of the quadratic form A33
A33 = Aq[:2, :2]
# determinant of A33
detA33 = np.linalg.det(A33)
if detA33 > 0:
raise ValueError('coeffs do not represent a hyperbola: det A33 must be negative!')
# get the center
x0 = -np.linalg.det(np.array([Aq[:2, 2], Aq[:2, 1]]).T) / detA33
y0 = -np.linalg.det(np.array([Aq[:2, 0], Aq[:2, 2]]).T) / detA33
# The semi-major and semi-minor axis lengths (these are not sorted).
# get discriminant of the conic section
delta = np.linalg.det(Aq)
# get the eigenvalues
k1, k2 = np.linalg.eigvals(A33)
k1isk2 = np.isclose(k1/k2, -1)
ap = np.sqrt(abs(delta/k1/detA33))
bp = np.sqrt(abs(delta/k2/detA33))
# Eccentricity.
fac = np.sqrt((Aq[0, 0] - Aq[1, 1])**2 + Aq[0, 1]**2)
if delta < 0:
nu = 1
else:
nu = -1
e = np.sqrt(2*fac/(nu*(Aq[0, 0] - Aq[1, 1]) + fac))
# slope of the asymptotes
if Aq[0, 0] == Aq[1, 1] and k1isk2:
m1 = 0.
m2 = np.nan
else:
m1 = Aq[0, 0]/(-Aq[0, 1] - np.sqrt(-detA33))
m2 = Aq[0, 0]/(-Aq[0, 1] + np.sqrt(-detA33))
# Sort the semi-major and semi-minor axis lengths but keep track of
# the original relative magnitudes of width and height.
width_gt_height = True
if ap < bp and not k1isk2:
width_gt_height = False
ap, bp = bp, ap
# The angle of anticlockwise rotation of the major-axis from x-axis.
if Aq[0, 1] == 0:
phi = 0 if Aq[0, 0] < Aq[1, 1] else np.pi/2
elif Aq[0, 0] == Aq[1, 1]:
phi = np.pi/4 # would divide by zero and arctan(inf) -> pi/4
if m1 > 0 and m2 > 0:
width_gt_height = True
else:# Aq[0, 0] > Aq[1, 1]:
phi = np.arctan(2*Aq[0, 1]/(Aq[0, 0] - Aq[1, 1])) / 2
if not width_gt_height:
# Ensure that phi is the angle to rotate to the semi-major axis.
phi += np.pi/2
phi = phi % np.pi
return x0, y0, ap, bp, phi, e, m1, m2, width_gt_height
def get_hyperbola_pts(params, npts=100, tmin=-1, tmax=1):
x0, y0, ap, bp, phi, m1, m2 = params
# A grid of the parametric variable, t.
t = np.linspace(tmin, tmax, npts)
# points
x = x0 + ap * np.cosh(t) * np.cos(phi) - bp * np.sinh(t) * np.sin(phi)
y = y0 + ap * np.cosh(t) * np.sin(phi) + bp * np.sinh(t) * np.cos(phi)
# asymptotes
ya1 = y0 + m1*(x - x0)
ya2 = y0 + m2*(x - x0)
return x, y, ya1, ya2
if __name__ == '__main__':
coeffs = [1., 6., -2., 3., 0., 0.]
x0, y0, ap, bp, phi, e, m1, m2, width_gt_height = hyp_params_from_general(coeffs)
print('x0, y0, ap, bp, phi, e, m1, m2, width_gt_height = ', x0, y0, ap, bp, phi, e, m1, m2)
x_, y_, ya1, ya2 = get_hyperbola_pts((x0, y0, ap, bp, phi, m1, m2), npts=250, tmin=-2, tmax=3)
fig, ax = plt.subplots(figsize=(16, 9))
ax.plot(x_, y_, marker='.', linewidth=0.5, c='r')
ax.plot(x_, ya1, marker='.', linewidth=0.2, c='b')
ax.plot(x_, ya2, marker='.', linewidth=0.2, c='b')
ax.grid(True, linestyle='--')
I have the general formula of an ellipsoid:
A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2 = 0
where A,B,C,D,E,F,G are constant factors.
How can I plot this equation as a 3D plot in matplotlib? (A wireframe would be best.)
I saw this example but it is in parametric form and I am not sure how to put the z-coordinates in this code. Is there a way to keep the general form to plot this without the parametric form?
I started to put this in some kind of code like this:
from mpl_toolkits import mplot3d
%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt
def f(x, y):
return ((A*x**2 + C*y**2 + D*x + E*y + B*x*y + F))
def f(z):
return G*z**2
x = np.linspace(-2200, 1850, 30)
y = np.linspace(-100, 60, 30)
z = np.linspace(-100, 60, 30)
X, Y, Z = np.meshgrid(x, y, z)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z');
I got this error:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-95b1296ae6a4> in <module>()
18 fig = plt.figure()
19 ax = fig.add_subplot(111, projection='3d')
---> 20 ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
21 ax.set_xlabel('x')
22 ax.set_ylabel('y')
C:\Program Files (x86)\Microsoft Visual Studio\Shared\Anaconda3_64\lib\site-packages\mpl_toolkits\mplot3d\axes3d.py in plot_wireframe(self, X, Y, Z, *args, **kwargs)
1847 had_data = self.has_data()
1848 if Z.ndim != 2:
-> 1849 raise ValueError("Argument Z must be 2-dimensional.")
1850 # FIXME: Support masked arrays
1851 X, Y, Z = np.broadcast_arrays(X, Y, Z)
ValueError: Argument Z must be 2-dimensional.
Side note, but what you have is not the most general equation for a 3d ellipsoid. Your equation can be rewritten as
A*x**2 + C*y**2 + D*x + E*y + B*x*y = - G*z**2 - F,
which means that in effect for each value of z you get a different level of a 2d ellipse, and the slices are symmetric with respect to the z = 0 plane. This shows how your ellipsoid is not general, and it helps check the results to make sure that what we get makes sense.
Assuming we take a general point r0 = [x0, y0, z0], you have
r0 # M # r0 + b0 # r0 + c0 == 0
where
M = [ A B/2 0
B/2 C 0
0 0 G],
b0 = [D, E, 0],
c0 = F
where # stands for matrix-vector or vector-vector product.
You could take your function and plot its isosurface, but that would be suboptimal: you would need a gridded approximation for your function which is very expensive to do to sufficient resolution, and you'd have to choose the domain for this sampling wisely.
Instead you can perform a principal axis transformation on your data to generalize the parametric plot of a canonical ellipsoid that you yourself linked.
The first step is to diagonalize M as M = V # D # V.T, where D is diagonal. Since it's a real symmetric matrix this is always possible and V is orthogonal. Then we have
r0 # V # D # V.T # r0 + b0 # r0 + c0 == 0
which we can regroup as
(V.T # r0) # D # (V.T # r0) + b0 # V # (V.T # r0) + c0 == 0
which motivates the definition of the auxiliary coordinates r1 = V.T # r0 and vector b1 = b0 # V, for which we get
r1 # D # r1 + b1 # r1 + c0 == 0.
Since D is a symmetric matrix with the eigenvalues d1, d2, d3 in its diagonal, the above is the equation
d1 * x1**2 + d2 * x2**2 + d3 * x3**3 + b11 * x1 + b12 * x2 + b13 * x3 + c0 == 0
where r1 = [x1, x2, x3] and b1 = [b11, b12, b13].
What's left is to switch from r1 to r2 such that we remove the linear terms:
d1 * (x1 + b11/(2*d1))**2 + d2 * (x2 + b12/(2*d2))**2 + d3 * (x3 + b13/(2*d3))**2 - b11**2/(4*d1) - b12**2/(4*d2) - b13**2/(4*d3) + c0 == 0
So we define
r2 = [x2, y2, z2]
x2 = x1 + b11/(2*d1)
y2 = y1 + b12/(2*d2)
z2 = z1 + b13/(2*d3)
c2 = b11**2/(4*d1) b12**2/(4*d2) b13**2/(4*d3) - c0.
For these we finally have
d1 * x2**2 + d2 * y2**2 + d3 * z2**2 == c2,
d1/c2 * x2**2 + d2/c2 * y2**2 + d3/c2 * z2**2 == 1
which is the canonical form of a second-order surface. In order for this to meaningfully correspond to an ellipsoid we must ensure that d1, d2, d3 and c2 are all strictly positive. If this is guaranteed then the semi-major axes of the canonical form are sqrt(c2/d1), sqrt(c2/d2) and sqrt(c2/d3).
So here's what we do:
ensure that the parameters correspond to an ellipsoid
generate a theta and phi mesh for polar and azimuthal angles
compute the transformed coordinates [x2, y2, z2]
shift them back (by r2 - r1) to get [x1, y1, z1]
transform the coordinates back by V to get r0, the actual [x, y, z] coordinates we're interested in.
Here's how I'd implement this:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def get_transforms(A, B, C, D, E, F, G):
""" Get transformation matrix and shift for a 3d ellipsoid
Assume A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2 = 0,
use principal axis transformation and verify that the inputs
correspond to an ellipsoid.
Returns: (d, V, s) tuple of arrays
d: shape (3,) of semi-major axes in the canonical form
(X/d1)**2 + (Y/d2)**2 + (Z/d3)**2 = 1
V: shape (3,3) of the eigensystem
s: shape (3,) shift from the linear terms
"""
# construct original matrix
M = np.array([[A, B/2, 0],
[B/2, C, 0],
[0, 0, G]])
# construct original linear coefficient vector
b0 = np.array([D, E, 0])
# constant term
c0 = F
# compute eigensystem
D, V = np.linalg.eig(M)
if (D <= 0).any():
raise ValueError("Parameter matrix is not positive definite!")
# transform the shift
b1 = b0 # V
# compute the final shift vector
s = b1 / (2 * D)
# compute the final constant term, also has to be positive
c2 = (b1**2 / (4 * D)).sum() - c0
if c2 <= 0:
print(b1, D, c0, c2)
raise ValueError("Constant in the canonical form is not positive!")
# compute the semi-major axes
d = np.sqrt(c2 / D)
return d, V, s
def get_ellipsoid_coordinates(A, B, C, D, E, F, G, n_theta=20, n_phi=40):
"""Compute coordinates of an ellipsoid on an ellipsoidal grid
Returns: x, y, z arrays of shape (n_theta, n_phi)
"""
# get canonical grid
theta,phi = np.mgrid[0:np.pi:n_theta*1j, 0:2*np.pi:n_phi*1j]
r2 = np.array([np.sin(theta) * np.cos(phi),
np.sin(theta) * np.sin(phi),
np.cos(theta)]) # shape (3, n_theta, n_phi)
# get transformation data
d, V, s = get_transforms(A, B, C, D, E, F, G) # could be *args I guess
# shift and transform back the coordinates
r1 = d[:,None,None]*r2 - s[:,None,None] # broadcast along first of three axes
r0 = (V # r1.reshape(3, -1)).reshape(r1.shape) # shape (3, n_theta, n_phi)
return r0 # unpackable to x, y, z of shape (n_theta, n_phi)
Here's an example with an ellipsoid and proof that it works:
A,B,C,D,E,F,G = args = 2, -1, 2, 3, -4, -3, 4
x,y,z = get_ellipsoid_coordinates(*args)
print(np.allclose(A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2, 0)) # True
The actual plotting from here is trivial. Using the 3d scaling hack from this answer to preserve equal axes:
# create 3d axes
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# plot the data
ax.plot_wireframe(x, y, z)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
# scaling hack
bbox_min = np.min([x, y, z])
bbox_max = np.max([x, y, z])
ax.auto_scale_xyz([bbox_min, bbox_max], [bbox_min, bbox_max], [bbox_min, bbox_max])
plt.show()
Here's how the result looks:
Rotating it around it's nicely visible that the surface is indeed reflection symmetric with respect to the z = 0 plane, which was evident from the equation.
You can change the n_theta and n_phi keyword arguments to the function to generate a grid with a different mesh. The fun thing is that you can take any scattered points lying on the unit sphere and plug it into the definition of r2 in the function get_ellipsoid_coordinates (as long as this array has a first dimension of size 3), and the output coordinates will have the same shape, but they will be transformed onto the actual ellipsoid.
You can also use other libraries to visualize the surface, for instance mayavi where you can either plot the surface we just computed, or compare it with an isosurface which is built-in there.
I generated the coordinates of a cylinder. Its two faces connect two arbitrary points already given.
Is it possible to build a 3D numpy mask of the filled cylinder from the coordinates with standard Python libraries? Creating a 2D mask seems simple enough, but I'm encountering some difficulties with 3D.
Here the code for generating the cylinder, taken from here and here:
import scipy
import scipy.linalg
import numpy as np
import nibabel as nib
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# defining mask
shape = (100, 100, 100)
image = np.zeros(shape=shape)
# set radius and centres values
r = 3
start = [30, 45, 60]
end = [40, 58, 70]
p1 = np.array(start)
p2 = np.array(end)
# # calculate p2-p1 distance
# dx = p2[0] - p1[0]
# dy = p2[1] - p1[1]
# dz = p2[2] - p1[2]
# dist = math.sqrt(dx**2 + dy**2 + dz**2)
# vector in direction of axis
v = p2 - p1
# find magnitude of vector
mag = scipy.linalg.norm(v)
# unit vector in direction of axis
v = v / mag
# make some vector not in the same direction as v
not_v = np.array([1, 0, 0])
if (v == not_v).all():
not_v = np.array([0, 1, 0])
# make vector perpendicular to v
n1 = np.cross(v, not_v)
# normalize n1
n1 /= scipy.linalg.norm(n1)
# make unit vector perpendicular to v and n1
n2 = np.cross(v, n1)
#surface ranges over t from 0 to length of axis and 0 to 2*pi
t = np.linspace(0, mag, 100)
theta = np.linspace(0, 2 * np.pi, 100)
rsample = np.linspace(0, r, 2)
#use meshgrid to make 2d arrays
t, theta2 = np.meshgrid(t, theta)
rsample, theta = np.meshgrid(rsample, theta)
# generate coordinates for surface
# "Tube"
X, Y, Z = [p1[i] + v[i] * t + r * np.sin(theta2) * n1[i] + r * np.cos(theta2) * n2[i] for i in [0, 1, 2]]
# "Bottom"
X2, Y2, Z2 = [p1[i] + rsample[i] * np.sin(theta) * n1[i] + rsample[i] * np.cos(theta) * n2[i] for i in [0, 1, 2]]
# "Top"
X3, Y3, Z3 = [p1[i] + v[i] * mag + rsample[i] * np.sin(theta) * n1[i] + rsample[i] * np.cos(theta) * n2[i] for i in [0, 1, 2]]
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z)
ax.plot_surface(X2, Y2, Z2)
ax.plot_surface(X3, Y3, Z3)
plt.show()
I need the 3D numpy mask to select all the values inside the cylinder of a 3D image. The shape of mask and image is the same.
In the end I looped through the coordinates of tube and faces.
I got the coordinates following this link: 3D points from Numpy meshgrid coordinates
tube = np.stack((X.ravel(), Y.ravel(), Z.ravel()), axis=1)
face1 = np.stack((X2.ravel(), Y2.ravel(), Z2.ravel()), axis=1)
face2 = np.stack((X3.ravel(), Y3.ravel(), Z3.ravel()), axis=1)
# filling numpy mask
for i in range(len(tube)):
image[int(tube[i][0]), int(tube[i][1]), int(tube[i][2])] = 255
for j in range(len(face1)):
image[int(face1[j][0]), int(face1[j][1]), int(face1[j][2])] = 255
for k in range(len(face2)):
image[int(face2[k][0]), int(face2[k][1]), int(face2[k][2])] = 255
mask_new = nib.Nifti1Image(image.astype(np.float32), ctsurg_file.affine)
nib.save(mask_new, os.path.join(currdir, 'mask_cyl.nii.gz'))
If I use matplotlib to generate a delaunay triangulation for a group of points, what is the most appropraite way of getting the circumcentres of the triangles that have been geenrated? I haven't yet managed to find an obvious method in the Triangulation library to do this.
You should be able to calculate it using matplotlib.delaunay.triangulate.Triangulation:
Triangulation(x, y)
x, y -- the coordinates of the points as 1-D arrays of floats
.
.
.
Attributes: (all should be treated as
read-only to maintain consistency)
x, y -- the coordinates of the points as 1-D arrays of floats.
circumcenters -- (ntriangles, 2) array of floats giving the (x,y)
coordinates of the circumcenters of each triangle (indexed by a triangle_id).
Adapted from one of the matplotlib examples (there is probably a cleaner way to do this, but it should work):
import matplotlib.pyplot as plt
import matplotlib.delaunay
import matplotlib.tri as tri
import numpy as np
import math
# Creating a Triangulation without specifying the triangles results in the
# Delaunay triangulation of the points.
# First create the x and y coordinates of the points.
n_angles = 36
n_radii = 8
min_radius = 0.25
radii = np.linspace(min_radius, 0.95, n_radii)
angles = np.linspace(0, 2*math.pi, n_angles, endpoint=False)
angles = np.repeat(angles[...,np.newaxis], n_radii, axis=1)
angles[:,1::2] += math.pi/n_angles
x = (radii*np.cos(angles)).flatten()
y = (radii*np.sin(angles)).flatten()
tt = matplotlib.delaunay.triangulate.Triangulation(x,y)
triang = tri.Triangulation(x, y)
# Plot the triangulation.
plt.figure()
plt.gca().set_aspect('equal')
plt.triplot(triang, 'bo-')
plt.plot(tt.circumcenters[:,0],tt.circumcenters[:,1],'r.')
plt.show()
Here is a function that computes them. It can also be used on other triangulation structures, e.g. scipy's Delaunay triangulation (see below).
def compute_triangle_circumcenters(xy_pts, tri_arr):
"""
Compute the centers of the circumscribing circle of each triangle in a triangulation.
:param np.array xy_pts : points array of shape (n, 2)
:param np.array tri_arr : triangles array of shape (m, 3), each row is a triple of indices in the xy_pts array
:return: circumcenter points array of shape (m, 2)
"""
tri_pts = xy_pts[tri_arr] # (m, 3, 2) - triangles as points (not indices)
# finding the circumcenter (x, y) of a triangle defined by three points:
# (x-x0)**2 + (y-y0)**2 = (x-x1)**2 + (y-y1)**2
# (x-x0)**2 + (y-y0)**2 = (x-x2)**2 + (y-y2)**2
#
# becomes two linear equations (squares are canceled):
# 2(x1-x0)*x + 2(y1-y0)*y = (x1**2 + y1**2) - (x0**2 + y0**2)
# 2(x2-x0)*x + 2(y2-y0)*y = (x2**2 + y2**2) - (x0**2 + y0**2)
a = 2 * (tri_pts[:, 1, 0] - tri_pts[:, 0, 0])
b = 2 * (tri_pts[:, 1, 1] - tri_pts[:, 0, 1])
c = 2 * (tri_pts[:, 2, 0] - tri_pts[:, 0, 0])
d = 2 * (tri_pts[:, 2, 1] - tri_pts[:, 0, 1])
v1 = (tri_pts[:, 1, 0] ** 2 + tri_pts[:, 1, 1] ** 2) - (tri_pts[:, 0, 0] ** 2 + tri_pts[:, 0, 1] ** 2)
v2 = (tri_pts[:, 2, 0] ** 2 + tri_pts[:, 2, 1] ** 2) - (tri_pts[:, 0, 0] ** 2 + tri_pts[:, 0, 1] ** 2)
# solve 2x2 system (see https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_2_%C3%97_2_matrices)
det = (a * d - b * c)
detx = (v1 * d - v2 * b)
dety = (a * v2 - c * v1)
x = detx / det
y = dety / det
return (np.vstack((x, y))).T
On the data from #JoshAdel's answer above, adding the following code:
cc = compute_triangle_circumcenters(np.vstack([tt.x, tt.y]).T, tt.triangle_nodes)
plt.plot(cc[:, 0], cc[:, 1], ".k")
I get the following figure:
It can also be used on scipy.spatial.Delaunay like this:
from scipy.spatial import Delaunay
xy_pts = np.vstack([x, y]).T
dt = Delaunay(xy_pts)
cc = compute_triangle_circumcenters(dt.points, dt.simplices)