The aim of this notebook is twofold:
import numpy as np
import plotly.graph_objects as go
A 3d arrow is designed as a right cone and a disk as its base. We define the standard cone, as the cone of vertex, Vert(0,0, headsize)
, and angle theta
between the symmetry axis, Oz, and any generatrice:
def arrow3d(headsize, theta):
r = headsize*np.tan(theta)
u = np.linspace(0,2*np.pi, 60)
v = np.linspace(0, 1, 15)
U,V = np.meshgrid(u,v)
#parameterization of the standard cone
x = r*V*np.cos(U)
y = r*V*np.sin(U)
z = headsize*(1-V)
cone = np.stack((x,y,z)) #shape(3, m, n)
w = np.linspace(0, r, 10)
u, w = np.meshgrid(u,w)
#parameterization of the base disk
xx = w*np.cos(u)
yy = w*np.sin(u)
zz = np.zeros(w.shape)
disk = np.stack((xx,yy,zz))
return cone, disk
Place a 3d arrow along a line, starting from a point on that line, called origin
below:
def place_arrow3d(start, end, headsize, theta):
# Move the standard arrow to a position in the 3d space,
# which is computed from the inputted data
# start = array of shape (3,) = the starting point of the arrow's support line
# end = array of shape(3, ) = the end point of the segment of line
# headsize
# theta=the angle between the symmetry axis and a generatrice
epsilon=1.0e-04 # any coordinate less than epsilon is considered 0
cone, disk = arrow3d(headsize, theta)#get the standard cone
arr_dir = end-start# the arrow direction
if np.linalg.norm(arr_dir) > epsilon:
#define a right orthonormal basis (u1, u2, u3), with u3 the unit vector of the arrow_dir
u3 = arr_dir/np.linalg.norm(arr_dir)
origin = end-headsize * u3 #the point where the arrow starts on the supp line
a, b, c = u3
if abs(a) > epsilon or abs(b) > epsilon:
v1 = np.array([-b, a, 0])# v1 orthogonal to u3
u1 = v1/np.linalg.norm(v1)
else:
u1 = np.array([1., 0, 0])
u2 = np.cross(u3, u1)# this def ensures that the orthonormal basis is a right one
T = np.vstack((u1, u2, u3)).T #Transformation T, T(e_i)=u_i, to be applied to the standard cone
cone = np.einsum('ji, imn -> jmn', T, cone)#Transform the standard cone
disk = np.einsum('ji, imn -> jmn', T, disk)#Transform the cone base
cone = np.apply_along_axis(lambda a, v: a+v, 0, cone, origin)#translate the cone;
#dir translation, v=vec(O,origin)
disk = np.apply_along_axis(lambda a, v: a+v, 0, disk, origin)# translate the cone base
return origin, cone, disk
else: return (0, )
Parameterize the Moebius strip and define it as a Plotly surface:
u = np.linspace(0, 2*np.pi, 36)
v = np.linspace(-0.5, 0.5, 10)
u, v = np.meshgrid(u,v)
tp = 1+v*np.cos(u/2.)
x = tp*np.cos(u)
y = tp*np.sin(u)
z = v*np.sin(u/2.)
fig= go.Figure(go.Surface(
x=x,
y=y,
z=z,
colorscale="balance",
colorbar=dict(thickness=20, len=0.6)))
Define a unicolor colorscale, to plot the cones and disks defining the 3d arrows:
pl_c = [[0.0, 'rgb(179, 56, 38)'],
[1.0, 'rgb(179, 56, 38)']]
The following function returns the Plotly traces that represent a 3d arrow:
def get_normals(start, origin, cone, disk, colorscale=pl_c):
tr_cone=go.Surface(
x=cone[0, :, :],
y=cone[1, :, :],
z=cone[2, :, :],
colorscale=colorscale,
showscale=False)
tr_disk=go.Surface(
x=disk[0, :, :],
y=disk[1, :, :],
z=disk[2, :, :],
colorscale=colorscale,
showscale=False)
tr_line=go.Scatter3d(
x=[start[0], origin[0]],
y=[start[1], origin[1]],
z=[start[2], origin[2]],
mode='lines',
line=dict(width=3, color='rgb(60, 9, 17)')
)
return [tr_line, tr_cone, tr_disk] #return a list that is concatenated to data
Define the normals along the central circle, i.e. the curve corresponding to v=0 in the Moebius strip parameterization:
u = np.linspace(0, 2*np.pi, 24)
xx = np.cos(u)
yy = np.sin(u)
zz = np.zeros(xx.shape)
starters = np.vstack((xx,yy,zz)).T
a = 0.3
#Normal coordinates
Nx = 2*np.cos(u)*np.sin(u/2)
Ny = np.cos(u/2)-np.cos(3*u/2)
Nz = -2*np.cos(u)
ends = starters+a*np.vstack((Nx,Ny, Nz)).T
for j in range(ends.shape[0]):
arr=place_arrow3d(starters[j], ends[j], 0.15, np.pi/15)
if len(arr)==3:# get normals at the regular points on a surface, i.e. where ||Normalvector|| not = 0
fig.add_traces(get_normals(starters[j], arr[0], arr[1], arr[2]))
fig.update_layout(title_text='<br>A vector field along the central circle of the Moebius strip',
title_x=0.5,
font_family='Balto',
width=675,
height=675,
showlegend=False,
scene=dict(camera_eye=dict(x=1.65, y=1.65, z=0.75),
aspectmode='data'))
from IPython.core.display import HTML
def css_styling():
styles = open("./custom.css", "r").read()
return HTML(styles)
css_styling()