Fernet Frames on Helix¶
A helix can be desribred as the set points
$$(x,y,z) = (r \cos(\theta), r \sin(\theta), t \cdot \theta))$$where the constants $r,t$ describe the radius & slope.
It has tangent vector $$ T = (r -\sin(\theta), r \cos(\theta), t). $$
The normal vector $N$ can be computed by taking the coordinate-wise derivative of $T$ and then dividing by its length
$$ N = \frac{\dot{T}}{|\dot{T}|}$$The bi-normal $B$ is perpendicular to both and can be computed as
$$ B = T \times N .$$The collection $\{T,N,B\}$ is called a Fernet frame. Each point of the helix has a such a set of orthogonal vectors based there and below we visualize the transition from one frame to another.
# import libraries
import os
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
import plotly.colors as pcolors
import plotly.offline as pyo
# Initialize Plotly offline mode, so we can display plots of the notebook in the .html file
pio.renderers.default = 'notebook'
pyo.init_notebook_mode()
# set theme
# pio.templates.default = "plotly_dark"
Define Variables¶
# Get a list of colors at specific intervals to color the frames along the helix
# sample_points = [0, 0.4, 0.75] # darker colors for T, N, B
sample_points = [1.0, 0.6, 0.4] # lighter colors for T, N, B
sampled_colors = pcolors.sample_colorscale(pcolors.sequential.Viridis, samplepoints=sample_points)
# Define parameters for the helix
num_points = 700
num_turns = 5
radius = 1
pitch = 0.5
t = np.linspace(-num_turns * np.pi, num_turns * np.pi, num_points)
# s subset of t to animate over
N = 400
num_s_points = N
s = np.linspace(min(t), max(t), N)
# Calculate the x, y, and z coordinates
x = radius * np.cos(t)
y = radius * np.sin(t)
z = pitch * t
# subset coordinates for animation
# for drawing the Frenet frame at each point on the helix
xx = radius * np.cos(s)
yy = radius * np.sin(s)
zz = pitch * s
# for drawing with fixed axes
xm = np.min(x) - 1.5
xM = np.max(x) + 1.5
ym = np.min(y) - 1.5
yM = np.max(y) + 1.5
zm = np.min(z) - 1.5
zM = np.max(z) + 1.5
Compute Frenet Frame¶
# compute Frenet frame
def create_frenet_frame(t, radius, pitch, k):
"""
Create the Frenet-Serret frame (Tangent, Normal, Binormal) at a point on the helix curve.
"""
# Tangent vector
dx = -radius * np.sin(t[k])
dy = radius * np.cos(t[k])
dz = pitch
T = np.array([dx, dy, dz])
T = T / np.linalg.norm(T)
# Normal vector
ddx = -radius * np.cos(t[k])
ddy = -radius * np.sin(t[k])
ddz = 0
N = np.array([ddx, ddy, ddz])
N = N / np.linalg.norm(N)
# Binormal vector
B = np.cross(T, N)
B = B / np.linalg.norm(B)
return T, N, B
T = np.empty((3,0))
N = np.empty((3,0))
B = np.empty((3,0))
for k in range(num_s_points):
T_k, N_k, B_k = create_frenet_frame(s, radius, pitch, k)
T = np.column_stack((T, T_k))
N = np.column_stack((N, N_k))
B = np.column_stack((B, B_k))
Coordinates of T,N,B¶
vx = T[0, :] # (vx, vy) is the velocity vector
vy = T[1, :] # (vx, vy) is the velocity vector
vz = T[2, :] # (vx, vy, vz) is the velocity vector
speed = np.sqrt(vx ** 2 + vy ** 2, vz ** 2) # speed = |velocity| = sqrt(vx^2 + vy^2 + vz^2)
ux = vx / speed # (ux, uy) unit tangent vector, (-uy, ux) unit normal vector
uy = vy / speed
uz = vz / speed
xend = xx + ux # end coordinates for the unit tangent vector at (xx, yy)
yend = yy + uy
zend = zz + uz
xnoe = xx + N[0, :] # end coordinates for the unit normal vector at (xx,yy)
ynoe = yy + N[1, :]
znoe = zz - N[2, :]
xboe = xx + B[0, :] # end coordinates for the unit binormal vector at (xx,yy)
yboe = yy + B[1, :]
zboe = zz + B[2, :]
First Create Static Figure¶
For plotting animations, it's important to keep in mind that that the base plots needs a copy of everything you want to animate. So since we want to see the frames move along the curve, we need a $\textit{trace}$ for both the frame and the curve. We need separate traces for the {T,N,B} because we want to paint each one a different color.
# Create figure
frames = []
for k in range(num_s_points):
frames.append(
go.Frame(
data=[
# Draw the helix curve
go.Scatter3d(x=x, y=y, z=z,
name="curve",
mode="lines",
line=dict(width=8, color="blue")),
# Draw the Tangent vector
go.Scatter3d(
x=[xx[k], xend[k]],
y=[yy[k], yend[k]],
z=[zz[k], zend[k]],
mode="lines",
line=dict(
color=sampled_colors[0], # Numerical array for color mapping
width=8)),
# Draw the Normal vector
go.Scatter3d(
x=[xx[k], xnoe[k]],
y=[yy[k], ynoe[k]],
z=[zz[k], znoe[k]],
mode="lines",
line=dict(
color=sampled_colors[1], # Numerical array for color mapping
width=8)) ,
# Draw the Binormal vector
go.Scatter3d(
x=[xx[k], xboe[k]],
y=[yy[k], yboe[k]],
z=[zz[k], zboe[k]],
mode="lines",
line=dict(
color=sampled_colors[2], # Numerical array for color mapping
width=8))
],
traces=[0, 1, 2,3],
name=f"frame{k}"
)
)
Next Create the Individual Frames¶
fig = go.Figure(
data=[
go.Scatter3d(x=x, y=y, z=z,
name="curve",
mode="lines",
line=dict(width=8, color="blue")),
# Initial state of the Tangent vector
go.Scatter3d(x=[xx[0], xend[0]],
y=[yy[0], yend[0]],
z=[zz[0], zend[0]],
mode="lines",
line=dict(color=sampled_colors[0], width=8),
name="Tangent"),
# Initial state of the Normal vector
go.Scatter3d(x=[xx[0], xnoe[0]],
y=[yy[0], ynoe[0]],
z=[zz[0], znoe[0]],
mode="lines",
line=dict(color=sampled_colors[1], width=8),
name="Normal"),
# Initial state of the Binormal vector
go.Scatter3d(x=[xx[0], xboe[0]],
y=[yy[0], yboe[0]],
z=[zz[0], zboe[0]],
mode="lines",
line=dict(color=sampled_colors[2], width=8),
name="Binormal")
],
layout=go.Layout(
scene=dict(
xaxis=dict(range=[xm, xM], autorange=False, zeroline=False),
yaxis=dict(range=[ym, yM], autorange=False, zeroline=False),
zaxis=dict(range=[zm, zM], autorange=False, zeroline=False),
aspectratio=dict(x=1, y=1, z=2)),
title=dict(text="Moving Frenet Frame Along a Helix"),
hovermode="closest",
updatemenus=[
dict(type="buttons",
buttons=[
dict(label="Play",
method="animate",
args=[None]),
dict(label="Pause",
method="animate",
args=[[None], {"frame": {"duration": 0, "redraw": False},
"mode": "immediate",
"transition": {"duration": 0}}])
])
],
),
frames=frames
)
fig.show()