Normals along the central circle of the Moebius strip

The aim of this notebook is twofold:

  • first, to show how we can define a standard 3d arrow and move it to be placed at different positions in space;
  • second, to illustrate what the non-orientability of this surface means.
In [34]:
import numpy as np
import plotly.plotly as py

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:

In [35]:
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:

In [36]:
def place_arrow3d(start, end, headsize, theta):
    #Move the standard arrow to a position in the 3d space, 
    #that 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, )
    

Set the layout of the Plotly plot:

In [37]:
axis = dict(
showbackground=True, 
backgroundcolor="rgb(230, 230,230)",
gridcolor="rgb(255, 255, 255)",      
zerolinecolor="rgb(255, 255, 255)",  
    )

layout = dict(title='<br>A vector field along the central circle of the Moebius strip',
              font=dict(family='Balto'),
              autosize=False,
              width=700,
              height=700,
              showlegend=False,
              scene=dict(camera = dict(eye=dict(x=1.25, y=1.25, z=0.55)),
                         aspectratio=dict(x=1, y=1, z=0.5),
                         xaxis=axis,
                         yaxis=axis, 
                         zaxis=dict(axis, **{'tickvals':[-0.6, -0.3, 0, 0.3, 0.6]}),
                        )
               )

Colorscale for the Moebius strip:

In [38]:
pl_balance=[[0.0, 'rgb(23, 28, 66)'], #  cmocean
            [0.1, 'rgb(41, 61, 150)'],
            [0.2, 'rgb(21, 112, 187)'],
            [0.3, 'rgb(89, 155, 186)'],
            [0.4, 'rgb(169, 194, 202)'],
            [0.5, 'rgb(240, 236, 235)'],
            [0.6, 'rgb(219, 177, 163)'],
            [0.7, 'rgb(201, 118, 90)'],
            [0.8, 'rgb(179, 56, 38)'],
            [0.9, 'rgb(125, 13, 41)'],
            [1.0, 'rgb(60, 9, 17)']
           ]

Parameterize the Moebius strip and define it as a Plotly surface:

In [39]:
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.)
moebius=dict(type='surface',
            x=x,
            y=y,
            z=z,
            colorscale=pl_balance,
            showscale=True,
            colorbar=dict(thickness=20, lenmode='fraction', len=0.75, ticklen=4))

Define a unicolor colorscale, to plot the cones and disks defining the 3d arrows:

In [40]:
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:

In [41]:
def get_normals(start, origin, cone, disk, colorscale=pl_c):
    tr_cone=dict(type='surface', 
                 x=cone[0,:,:],
                 y=cone[1,:,:],
                 z=cone[2,:,:],
                 colorscale=colorscale,
                 showscale=False)
    tr_disk=dict(type='surface',
                 x=disk[0,:,:],
                 y=disk[1,:,:],
                 z=disk[2,:,:],
                 colorscale=colorscale,
                 showscale=False)
    tr_line=dict(type='scatter3d',
                 x=[start[0], origin[0]],
                 y=[start[1], origin[1]],
                 z=[start[2], origin[2]],
                 name='', 
                 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:

In [42]:
u=np.linspace(0, 2*np.pi, 36)
xx=np.cos(u)
yy=np.sin(u)
zz=np.zeros(xx.shape)
starters=np.vstack((xx,yy,zz)).T
a=0.35
#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
In [43]:
data=[moebius]
In [44]:
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
        data+=get_normals(starters[j], arr[0], arr[1], arr[2])
In [45]:
fig=dict(data=data, layout=layout)
py.sign_in('empet','api_key' )
py.iplot(fig, filename='normalsMob')
The draw time for this plot will be slow for all clients.
Out[45]:
In [46]:
from IPython.core.display import HTML
def  css_styling():
    styles = open("./custom.css", "r").read()
    return HTML(styles)
css_styling()
Out[46]: