Growing tube model#
In this tutorial, you will generate shells with the growing tube model (Okamoto, 1988).
The growing tube model describes a coiling pattern using a differential-geometric framework.
The tube radius and the trajectory’s local geometry are set by three parameters
at each growth stage s:
expansion rate
e_g: how fast the tube radius growsstandardized curvature
c_g: how tightly the trajectory bends (c_g = 0is a straight tube)standardized torsion
t_g: how much the trajectory twists out of a plane (t_g = 0is a planispiral)
Setup#
# Uncomment if needed
# %pip install "ktch[plot]"
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from ktch.coiling import GrowingTubeModel, growing_tube, l_g, s_g
Generate a shell with the growing tube model#
growing_tube takes the three parameters and returns a sampled surface.
s = np.linspace(0.0, 60.0, 200)
phi = np.linspace(0.0, 2.0 * np.pi, 60)
X = growing_tube(e_g=0.02, c_g=0.4, t_g=0.06, s_range=s, phi_range=phi)
X.shape
(200, 60, 3)
The result is a (n_s, n_phi, 3) array: n_s samples along the growth stage
s, n_phi samples around the tube, and the (x, y, z) coordinates.
fig = plot_surface3d(X, title="Growing tube (e_g=0.02, c_g=0.4, t_g=0.06)")
fig.show()
Explore the parameters#
We vary one parameter at a time.
Expansion rate e_g#
With e_g = 0 the tube keeps a constant radius; larger values open the shell up quickly.
compare_growing_tube([0.0, 0.04, 0.2], vary="e_g", title="Expansion rate",
c_g=0.4, t_g=0.06).show()
Standardized curvature c_g#
Larger values give a tighter coil.
(At c_g = 0 the trajectory becomes straight.)
compare_growing_tube([0.0, 0.4, 0.8], vary="c_g", title="Standardized curvature",
e_g=0.04, t_g=0.06).show()
Standardized torsion t_g#
With t_g = 0 the coil stays planar (a planispiral); increasing it lifts it into a helix.
compare_growing_tube([0.0, 0.06, 0.2], vary="t_g", title="Standardized torsion",
e_g=0.04, c_g=0.4).show()
Sample evenly along the shell#
By default the surface is sampled at equal steps in the growth stage s.
Because the tube radius grows exponentially with s, equal s steps trace
little arc length near the tight apex and much more near the wide aperture.
For even meshes, sample at equal steps of arc length along the shell instead.
l_g and s_g convert between the growth stage and the trajectory arc length
l. The arc length depends only on the expansion rate e_g (and r0), not on
the curvature c_g or torsion t_g. To space n_s samples evenly in arc
length, take a uniform grid in l and map it back to s with s_g:
e_g, c_g, t_g = 0.04, 0.4, 0.06
d_g = np.hypot(c_g, t_g)
n_whorls = 4.0
n_s = 60
phi = np.linspace(0.0, 2.0 * np.pi, 40)
s_uniform = np.linspace(0.0, 2.0 * np.pi * n_whorls / d_g, n_s)
l_max = l_g(s_uniform[-1], e_g)
s_arclength = s_g(np.linspace(0.0, l_max, n_s), e_g)
fig = make_subplots(
rows=1,
cols=2,
specs=[[{"type": "surface"}, {"type": "surface"}]],
subplot_titles=["equal steps in s", "equal steps in arc length"],
horizontal_spacing=0.01,
)
for col, s in enumerate([s_uniform, s_arclength], start=1):
Xi = growing_tube(e_g, c_g, t_g, s_range=s, phi_range=phi)
fig.add_trace(
go.Surface(
x=Xi[..., 0],
y=Xi[..., 1],
z=Xi[..., 2],
showscale=False,
colorscale="Viridis",
),
row=1,
col=col,
)
fig.update_layout(
width=900,
height=450,
margin=dict(l=0, r=0, t=40, b=0),
scene=dict(aspectmode="data"),
scene2=dict(aspectmode="data"),
)
fig.show()
Notice how the mesh on the right is spaced much more evenly along the coil.
Heteromorph#
So far the parameters were constant, giving regular spirals. The growing tube
model naturally allows its parameters (e_g, c_g, and t_g) to change during growth.
Pass each as a function of the growth stage s (or an array aligned to
s_range). This produces heteromorph (irregularly coiled) shells.
The example below mimics the heteromorph ammonite Nipponites (Okamoto, 1988): a constant expansion rate with curvature and torsion that oscillate along growth.
e_g = np.log(1.028)
def c_g(s):
return 0.2 * np.sin(2.0 * np.pi * s / 7.0 - np.pi / 2.0) + 0.5
def t_g(s):
return 0.55 * np.cos(2.0 * np.pi * s / 14.0)
Let’s look at the two varying parameters as functions of s:
s = np.linspace(0.0, 45.0, 451)
fig = go.Figure()
fig.add_scatter(x=s, y=c_g(s), mode="lines", name="c_g(s)")
fig.add_scatter(x=s, y=t_g(s), mode="lines", name="t_g(s)")
fig.update_layout(width=700, height=300, xaxis_title="s", yaxis_title="value")
fig.show()
Non-constant parameters require method="ode" (the closed form only handles
constant parameters) and an explicit s_range.
X = growing_tube(e_g, c_g, t_g, s_range=s, method="ode",
phi_range=np.linspace(0.0, 2.0 * np.pi, 40))
plot_surface3d(X, title="Nipponites-like heteromorph", colorscale="Purp").show()
Try changing the oscillation periods (7 and 14) or amplitudes to explore other irregular forms.
Generate shells in batch#
GrowingTubeModel provides a scikit-learn-style estimator whose
inverse_transform generates forms from parameters.
The method argument selects the solver ("ode" or the constant-parameter
"closed" form).
model = GrowingTubeModel()
params = np.array(
[
[0.05, 0.24, 0.05],
[0.03, 0.20, 0.00],
[0.10, 0.40, 0.15],
]
)
surfaces = model.inverse_transform(
params, s_range=np.linspace(0.0, 50.0, 180), phi_range=phi
)
surfaces.shape
(3, 180, 40, 3)
plot_surface3d(surfaces[2], title="Third parameter set").show()
See also
Raup’s model for Raup’s model.
Theoretical morphological models of coiling for the theory behind the growing tube model, including the differential geometry of heteromorph growth.
References#
Okamoto, T., 1988. Analysis of heteromorph ammonoids by differential geometry. Palaeontology 31, 35–52.