{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Normals along the central circle of the Moebius strip ##"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The aim of this notebook is twofold: \n",
"- first, to show how we can define a standard 3d arrow and place it at different positions in space;\n",
"- second, to illustrate the non-orientability of this surface."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import plotly.graph_objects as go"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def arrow3d(headsize, theta):\n",
" r = headsize*np.tan(theta)\n",
" u = np.linspace(0,2*np.pi, 60)\n",
" v = np.linspace(0, 1, 15)\n",
" U,V = np.meshgrid(u,v)\n",
" #parameterization of the standard cone \n",
" x = r*V*np.cos(U)\n",
" y = r*V*np.sin(U)\n",
" z = headsize*(1-V)\n",
" cone = np.stack((x,y,z)) #shape(3, m, n)\n",
" w = np.linspace(0, r, 10)\n",
" u, w = np.meshgrid(u,w)\n",
" #parameterization of the base disk\n",
" xx = w*np.cos(u)\n",
" yy = w*np.sin(u)\n",
" zz = np.zeros(w.shape)\n",
" disk = np.stack((xx,yy,zz))\n",
" return cone, disk"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Place a 3d arrow along a line, starting from a point on that line, called `origin` below:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def place_arrow3d(start, end, headsize, theta):\n",
" # Move the standard arrow to a position in the 3d space, \n",
" # which is computed from the inputted data\n",
" \n",
" # start = array of shape (3,) = the starting point of the arrow's support line\n",
" # end = array of shape(3, ) = the end point of the segment of line\n",
" # headsize\n",
" # theta=the angle between the symmetry axis and a generatrice\n",
" \n",
" epsilon=1.0e-04 # any coordinate less than epsilon is considered 0\n",
" \n",
" cone, disk = arrow3d(headsize, theta)#get the standard cone\n",
" arr_dir = end-start# the arrow direction\n",
" if np.linalg.norm(arr_dir) > epsilon:\n",
" #define a right orthonormal basis (u1, u2, u3), with u3 the unit vector of the arrow_dir\n",
" u3 = arr_dir/np.linalg.norm(arr_dir)\n",
" origin = end-headsize * u3 #the point where the arrow starts on the supp line\n",
" a, b, c = u3\n",
" if abs(a) > epsilon or abs(b) > epsilon:\n",
" v1 = np.array([-b, a, 0])# v1 orthogonal to u3\n",
" u1 = v1/np.linalg.norm(v1)\n",
" else: \n",
" u1 = np.array([1., 0, 0])\n",
" u2 = np.cross(u3, u1)# this def ensures that the orthonormal basis is a right one\n",
" T = np.vstack((u1, u2, u3)).T #Transformation T, T(e_i)=u_i, to be applied to the standard cone \n",
" cone = np.einsum('ji, imn -> jmn', T, cone)#Transform the standard cone\n",
" disk = np.einsum('ji, imn -> jmn', T, disk)#Transform the cone base\n",
" cone = np.apply_along_axis(lambda a, v: a+v, 0, cone, origin)#translate the cone; \n",
" #dir translation, v=vec(O,origin)\n",
" disk = np.apply_along_axis(lambda a, v: a+v, 0, disk, origin)# translate the cone base\n",
" return origin, cone, disk \n",
" \n",
" else: return (0, )\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Parameterize the Moebius strip and define it as a Plotly surface:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"u = np.linspace(0, 2*np.pi, 36)\n",
"v = np.linspace(-0.5, 0.5, 10)\n",
"u, v = np.meshgrid(u,v)\n",
"tp = 1+v*np.cos(u/2.)\n",
"x = tp*np.cos(u)\n",
"y = tp*np.sin(u)\n",
"z = v*np.sin(u/2.)\n",
"fig= go.Figure(go.Surface(\n",
" x=x,\n",
" y=y,\n",
" z=z,\n",
" colorscale=\"balance\",\n",
" colorbar=dict(thickness=20, len=0.6)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Define a unicolor colorscale, to plot the cones and disks defining the 3d arrows:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pl_c = [[0.0, 'rgb(179, 56, 38)'],\n",
" [1.0, 'rgb(179, 56, 38)']]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following function returns the Plotly traces that represent a 3d arrow:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_normals(start, origin, cone, disk, colorscale=pl_c):\n",
" tr_cone=go.Surface( \n",
" x=cone[0, :, :],\n",
" y=cone[1, :, :],\n",
" z=cone[2, :, :],\n",
" colorscale=colorscale,\n",
" showscale=False)\n",
" tr_disk=go.Surface(\n",
" x=disk[0, :, :],\n",
" y=disk[1, :, :],\n",
" z=disk[2, :, :],\n",
" colorscale=colorscale,\n",
" showscale=False)\n",
" tr_line=go.Scatter3d(\n",
" x=[start[0], origin[0]],\n",
" y=[start[1], origin[1]],\n",
" z=[start[2], origin[2]],\n",
" mode='lines',\n",
" line=dict(width=3, color='rgb(60, 9, 17)')\n",
" )\n",
" return [tr_line, tr_cone, tr_disk] #return a list that is concatenated to data \n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Define the normals along the central circle, i.e. the curve corresponding to v=0 in the Moebius strip parameterization:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"u = np.linspace(0, 2*np.pi, 24)\n",
"xx = np.cos(u)\n",
"yy = np.sin(u)\n",
"zz = np.zeros(xx.shape)\n",
"starters = np.vstack((xx,yy,zz)).T\n",
"a = 0.3\n",
"#Normal coordinates\n",
"Nx = 2*np.cos(u)*np.sin(u/2)\n",
"Ny = np.cos(u/2)-np.cos(3*u/2)\n",
"Nz = -2*np.cos(u)\n",
"ends = starters+a*np.vstack((Nx,Ny, Nz)).T"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for j in range(ends.shape[0]):\n",
" arr=place_arrow3d(starters[j], ends[j], 0.15, np.pi/15)\n",
" if len(arr)==3:# get normals at the regular points on a surface, i.e. where ||Normalvector|| not = 0\n",
" fig.add_traces(get_normals(starters[j], arr[0], arr[1], arr[2]))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"fig.update_layout(title_text='
A vector field along the central circle of the Moebius strip',\n",
" title_x=0.5,\n",
" font_family='Balto',\n",
" width=675,\n",
" height=675,\n",
" showlegend=False,\n",
" scene=dict(camera_eye=dict(x=1.65, y=1.65, z=0.75),\n",
" aspectmode='data'))\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Moebius-norm](Data/Moebius-normals.png)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from IPython.core.display import HTML\n",
"def css_styling():\n",
" styles = open(\"./custom.css\", \"r\").read()\n",
" return HTML(styles)\n",
"css_styling()"
]
}
],
"metadata": {
"@webio": {
"lastCommId": null,
"lastKernelId": null
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
}
},
"nbformat": 4,
"nbformat_minor": 1
}