{ "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 }