.ipynb

Sample evenly along a coiled shell#

Space the surface samples of a coiling model at equal steps of arc length along the shell instead of equal steps in the model’s growth parameter.

By default, ktch.coiling samples a shell at equal steps of the coiling angle theta (Raup’s model) or the growth stage s (growing tube model). Because the tube grows exponentially, equal steps give a dense sampling near the tight apex and a sparse one at the wide aperture. The conversion functions below map a uniform grid in arc length l back to the model’s parameter.

Prerequisites#

import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from ktch.coiling import (
    GrowingTubeModel,
    RaupModel,
    l_g,
    l_r,
    s_g,
    theta_r,
)

Raup’s model#

l_r gives the trajectory arc length at a coiling angle, and theta_r inverts it. Take a uniform grid in l up to the arc length of the last whorl and map it back to theta:

w_r, t_r, d_r = 2.0, 0.7, 0.05
n_whorls = 4.0
n_theta = 48

theta_uniform = np.linspace(0.0, 2.0 * np.pi * n_whorls, n_theta)

l_max = l_r(theta_uniform[-1], w_r, t_r, d_r)
theta_arclength = theta_r(np.linspace(0.0, l_max, n_theta), w_r, t_r, d_r)

Pass the converted grid as theta_range:

phi = np.linspace(0.0, 2.0 * np.pi, 40)
raup_model = RaupModel()

X_uniform = raup_model.inverse_transform(
    [w_r, t_r, d_r], theta_range=theta_uniform, phi_range=phi
)
X_arclength = raup_model.inverse_transform(
    [w_r, t_r, d_r], theta_range=theta_arclength, phi_range=phi
)

Growing tube model#

l_g and s_g convert between the growth stage and the arc length. The arc length depends only on the expansion rate e_g (and r0), not on the curvature c_g or torsion t_g:

e_g, c_g, t_g = 0.04, 0.4, 0.06
d_g = np.hypot(c_g, t_g)  # one whorl advances s by 2*pi / d_g
n_s = 60

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)

Pass the converted grid as s_range:

growing_tube_model = GrowingTubeModel(method="closed")

Y_uniform = growing_tube_model.inverse_transform(
    [e_g, c_g, t_g], s_range=s_uniform, phi_range=phi
)
Y_arclength = growing_tube_model.inverse_transform(
    [e_g, c_g, t_g], s_range=s_arclength, phi_range=phi
)

Visualization#

Compare the meshes side by side. The arc-length grids space the rings evenly along the coil.

panels = [
    (X_uniform, "Raup: equal steps in theta"),
    (X_arclength, "Raup: equal steps in arc length"),
    (Y_uniform, "Growing tube: equal steps in s"),
    (Y_arclength, "Growing tube: equal steps in arc length"),
]
fig = make_subplots(
    rows=2,
    cols=2,
    specs=[[{"type": "surface"}] * 2] * 2,
    subplot_titles=[title for _, title in panels],
    horizontal_spacing=0.01,
    vertical_spacing=0.05,
)
for i, (Xi, _) in enumerate(panels):
    if i < 2:  # Raup's model: show apex-up (rotation, not a z-flip)
        Xi = Xi * np.array([-1.0, 1.0, -1.0])
    fig.add_trace(
        go.Surface(
            x=Xi[..., 0], y=Xi[..., 1], z=Xi[..., 2],
            showscale=False, colorscale="Viridis",
        ),
        row=i // 2 + 1,
        col=i % 2 + 1,
    )
fig.update_layout(
    width=900,
    height=800,
    margin=dict(l=0, r=0, t=40, b=0),
    **{name: dict(aspectmode="data") for name in ["scene", "scene2", "scene3", "scene4"]},
)
fig.show()

The converted grids take large steps near the apex, where a revolution is short, and small steps near the aperture, where a revolution is long:

step_theta = np.degrees(np.diff(theta_arclength))
step_s = np.diff(s_arclength)
print(f"Raup, theta step:   first {step_theta[0]:.1f} deg, last {step_theta[-1]:.1f} deg")
print(f"Growing tube, s step: first {step_s[0]:.2f}, last {step_s[-1]:.2f}")
Raup, theta step:   first 143.9 deg, last 10.5 deg
Growing tube, s step: first 4.28, last 0.39

See also#