Shivam K. Panda · M. Khalid Jawed
University of California, Los Angeles
Elastic ribbons — slender structures whose length (L), width (W), and thickness (b) satisfy L ≫ W ≫ b — exhibit mechanical behaviors intermediate between one-dimensional rods (L ≫ W, b) and two-dimensional plates (L, W ≫ b). In quadratic Kirchhoff-type rod-based frameworks such as Discrete Elastic Rods (DER), the governing equilibrium equations are independent of width and therefore cannot capture width-dependent mechanical effects. Reduced centerline-based ribbon models attempt to capture width dependence via coupled bending–twisting energies; however, their relative accuracy remains unclear due to the absence of a unified simulation framework.
In this work, we formulate a framework grounded in discrete differential geometry where the energy is expressed as functions of coupled bending–twisting strain measures along the centerline, rather than a linear sum of quadratic bending and twisting energies in DER. We derive analytical gradients and Hessians of the energy that enable implicit time integration. Within this unified setting, we compare five ribbon models: Kirchhoff, Sadowsky, Wunderlich, Sano, and Audoly. As a benchmark, a straight ribbon is longitudinally constrained into a pre-buckled arch and subjected to transverse displacement, inducing a supercritical pitchfork bifurcation. Predicted bifurcation thresholds are compared against shell-based finite element simulations, with the Sano model providing the closest agreement in capturing width-dependent shifts. Our high-performance JAX-based implementation achieves O(N) per-iteration cost and confirms that the Sano model introduces negligible per-iteration overhead relative to standard DER.
This work is provided through two official repositories:
| Repository | Description |
|---|---|
discrete-elastic-ribbon |
Reference Python implementation. All five energy models, adaptive implicit Euler with a robust regularized linear solver, and a homotopy API for changing rod geometry/material during simulation. |
discrete-elastic-ribbon-jax |
High-performance JAX/Equinox implementation. Banded Hessian assembly with LAPACK banded factorization, vectorized stencil operations, and end-to-end differentiability through the implicit Newton–Raphson solver. |
You are reading the reference Python implementation. It provides a discrete differential geometry simulator for elastic ribbons with adaptive time-stepping, generalized energy formulations, and homotopic geometry/material updates during simulation.
This codebase was forked from PyDismech at commit 4f0eb69 and refactored for Discrete Elastic Ribbon. For the high-performance JAX/Equinox port, see discrete-elastic-ribbon-jax.
To install this Python library within a new virtual environment execute the following bash commands. If you wish to use your own package manager (conda), only execute the commands after the comment.
python -m venv .venv
source .venv/bin/activate # .venv/Script/activate for Windows
# after virtual environment setup
pip install -r requirements.txt
pip install -e . # Editable installation for developmentThe time stepper in src/dismech/time_steppers/time_stepper.py implements adaptive implicit Euler integration and a robust linear solver to handle stiff systems and near-singular Jacobians during bifurcation and large deformations.
REQUIRE: Initial state q₀, q̇₀; time step bounds h_min, h_max; tolerances ε_force, ε_disp
ENSURE: Equilibrium configuration q*
WHILE t < T:
Newton-Raphson iteration:
REPEAT
Compute F_int = -∇_q E, H_E = ∇²_q E from generalized energy
Assemble residual r = M Δq - h M q̇ - h² (F_int + F_ext)
Assemble Jacobian J = M - h² (H_E + ∂F_ext/∂q)
Δq_free ← RobustSolve(J_free, r_free) [Algorithm 2]
q ← q - Δq_free | free DOFs
UNTIL ‖r_free‖ < ε_force OR ‖Δq‖_∞/h < ε_disp
Adaptive time-stepping:
IF not converged:
h ← max(h/2, h_min); retry step
ELSIF ‖Δq⁽⁰⁾‖ small and few iterations:
h ← min(1.5h, h_max) [Increase step]
ENDIF
Update q̇, m₁ⁱ, m₂ⁱ, m_refⁱ; t ← t + h
RETURN q
REQUIRE: Jacobian J, residual r; condition threshold κ_max; regularization λ₀
ENSURE: Newton step Δq
IF cond(J) < κ_max:
RETURN J⁻¹ r [Direct solve]
λ ← λ₀
WHILE cond(J + λI) ≥ κ_max:
λ ← 10λ [Tikhonov-Miller regularization]
ENDWHILE
IF regularization sufficient:
RETURN (J + λI)⁻¹ r
ELSE:
RETURN J⁺ r [Truncated SVD pseudo-inverse fallback]
ENDIF
The RobustSolver (src/dismech/solvers/solver.py) applies Tikhonov regularization when the Jacobian is ill-conditioned (default threshold cond > 1e12) and falls back to an SVD-based pseudo-inverse if the regularized solve fails. Adaptive time-stepping reduces dt when Newton fails or when displacement per step exceeds max_dq_threshold, and increases dt when the system is stable.
Discrete Elastic Ribbon uses a generalized elastic energy framework that separates geometry-dependent strain computation from analytical strain-energy models. This allows plugging in different rod energy models (Kirchhoff, Sadowsky, Wunderlich, Sano, Audoly) by implementing only the analytical energy as a function of strains.
REQUIRE: State q, material frame (m₁, m₂), natural strains ε_nat
REQUIRE: Analytical model E(ε) with ∇_ε E and ∇²_ε E
ENSURE: Global force F, stiffness K
Initialize F ← 0, K ← 0
FOR each element k = 1, …, N:
Step 1: Compute local strains from geometry
Extract node positions (x₀, x₁, x₂)_k and twist angles (θ_e, θ_f)_k
Compute strain vector ε_k = [ε, κ⁽¹⁾, κ⁽²⁾, τ]_kᵀ
Compute strain gradients ∂ε_k/∂q_loc and Hessians ∂²ε_k/∂q_loc²
Step 2: Query analytical model in strain space
Δε_k ← ε_k - ε_nat,k [Delta strain]
Normalize: ε̃_k ← [Δε, Δκ⁽¹⁾(h/ℓ), Δκ⁽²⁾(h/ℓ), Δτ(h/ℓ)]_kᵀ
Evaluate E_k, ∇_ε̃ E_k, ∇²_ε̃ E_k from analytical model
Step 3: Chain rule to local DOF stencil
∇_q_loc E_k ← (∂ε_k/∂q_loc)ᵀ ∇_ε̃ E_k
∇²_q_loc E_k ← (∂ε_k/∂q_loc)ᵀ ∇²_ε̃ E_k (∂ε_k/∂q_loc) + Σ_i (∂E_k/∂ε_i)(∂²ε_i/∂q_loc²)
Step 4: Assemble to global DOFs
F[I_k] ← F[I_k] - ∇_q_loc E_k
K[I_k, I_k] ← K[I_k, I_k] - ∇²_q_loc E_k
ENDFOR
Implemented in src/dismech/elastics/general_elastic_energy.py and the model-specific GeneralElasticEnergy* classes.
| Model | Analytical Energy | General Elastic Energy |
|---|---|---|
| Kirchhoff | analytical_kirchhoff_elastic_energy.py |
general_elastic_energy_kirchhoff.py |
| Sadowsky | analytical_sadowsky_elastic_energy.py |
general_elastic_energy_sadowsky.py |
| Wunderlich | analytical_wunderlichs_elastic_energy.py |
general_elastic_energy_wunderlich.py |
| Sano | analytical_sanos_elastic_energy.py |
general_elastic_energy_sano.py |
| Audoly | analytical_audoly_elastic_energy.py |
general_elastic_energy_audoly.py |
Each analytical module provides the energy density E as a function of normalized strains ε̃ = [ϵ, κ₁, κ₂, τ], plus gradient and Hessian.
To add a new rod energy model:
-
Implement the analytical energy in
src/dismech/elastics/:- Create
analytical_<name>_elastic_energy.pywith a class that provides:forward(x)or equivalent: energy density E(ε̃) for inputxof shape(batch, 4).compute_energy_grad_hess(x): returns (E, gradient, Hessian) w.r.t. normalized strains.
- Create
-
Implement the general elastic energy wrapper:
- Create
general_elastic_energy_<name>.pythat:- Extends the base generalized energy framework.
- Calls your analytical model with the normalized strain vector ε̃ = [ϵ, κ₁, κ₂, τ].
- Assembles forces and stiffness via the chain rule as in Algorithm 3.
- Create
-
Register in the time stepper (
time_stepper.py):- Add the new model to the
energy_model_typedispatch and construct the analytical model with geometry-derived parameters (EA, EI1, EI2, GJ, delta_l, h, and any model-specific parameters like Sano's zeta or Wunderlich's W).
- Add the new model to the
Only the analytical energy expression E(ε̃) needs to be implemented; the rest is handled by the generalized framework.
The shear_induced_homotopy/simulate.py script implements the boundary value problem that isolates the shear-induced supercritical pitchfork bifurcation of a pre-buckled elastic ribbon.
Boundary condition: A ribbon (length L = 0.1 m, width W, thickness b) is clamped at both ends. First, one clamp is moved toward the other along the ribbon axis to apply longitudinal compression (about 25% of L), which buckles the ribbon. Then, with that pre-buckled state fixed, a transverse displacement (delta-W) is applied at one clamp to impose shear. The simulation drives these two loading phases via prescribed motion of the boundary nodes.
The same BVP is implemented in both shear_induced_bifurcation/simulate.py (compression + shear only, with optional tracking) and shear_induced_homotopy/simulate.py (adds a homotopy width-ramp phase; see Shear-Induced Homotopy Demo).
cd shear_induced_bifurcation
python simulate.py --config simulate_shear_induced_bifurcation_config.yaml --pkl-dir out/pkls --plot-dir out/plots
# Override W/L ratio(s) or node count(s) from config
python simulate.py --config simulate_shear_induced_bifurcation_config.yaml --pkl-dir out/pkls --plot-dir out/plots --wbyl 1/14 1/18 --nodes 21 63
# Override energy model (sano, wunderlich, kirchhoff, sadowsky, audoly)
python simulate.py --config simulate_shear_induced_bifurcation_config.yaml --pkl-dir out/pkls --plot-dir out/plots --energy-model sanoOutputs are written to --pkl-dir; plots (if generated) go to --plot-dir. Use --plot to run only missing cases then plot, instead of rerunning all.
- Phase 1 (move in x): The “start” nodes (e.g. x ≤ 0.01) are displaced along the longitudinal direction, inducing compression and pre-buckling.
- Phase 2 (move in y): The same boundary nodes are displaced transversely, imposing delta-W incrementally and inducing shear.
- Boundary conditions:
boundary_condition.start_x_thresholdandend_x_thresholddefine which nodes are clamped/ driven.u0is the displacement per step;motion_phasesdefine the time windows and directions (0=x, 1=y, 2=z) for each phase. - Gravity ramp: Strong gravity during
gravity_ramp_end_timesettles the rod; thengravity_after_ramp(often zero) isolates the elastic response.
| Section | Parameter | Description |
|---|---|---|
geometry |
L, w_by_l_ratio, h |
Ribbon length, width as fraction of L, thickness |
boundary_condition |
start_x_threshold, end_x_threshold |
Distance thresholds for start/end nodes |
boundary_condition |
u0 |
Prescribed displacement per step (m) |
boundary_condition |
motion_phases |
List of {start_time, end_time, direction, reverse} |
boundary_condition |
gravity_ramp_end_time, gravity_during_ramp, gravity_after_ramp |
Gravity for settling vs. elastic phase |
simulation |
dt, total_time, max_iter |
Time step, duration, Newton iteration limit |
adaptive_dt |
enabled, max_dq_threshold, dt_reduction_factor |
Adaptive time-stepping |
energy_model |
name |
One of: sano, wunderlich, kirchhoff, sadowsky, audoly |

Render demo: Sano W/L=1/12 to W/L=1/2
Discrete Elastic Ribbon extends PyDismech with a homotopy API that allows changing rod cross-section geometry (width, height) and material parameters during a simulation, without restarting or re-discretizing.
In bifurcation and path-following studies, one often needs to:
- Continuously vary cross-section dimensions along a solution branch
- Switch material properties to explore different regimes
- Interpolate between configurations during quasi-static or dynamic simulations
Homotopy enables this by mutating geometry, stiffness, and mass in place and propagating updates through springs and energy models.
| Component | Method | Purpose |
|---|---|---|
| SoftRobot | update_rod_geometry(width=..., height=...) |
Update rod cross-section (w×h). Mutates GeomParams, stiffness, springs, mass matrix. |
| SoftRobot | update_rod_material(density=..., youngs_rod=..., poisson_rod=...) |
Update material. Recomputes stiffness and mass, propagates to springs. |
| TimeStepper | refresh_rod_params(robot) |
Propagate rod changes into elastic energy models. Call after update_rod_geometry or update_rod_material. |
- In a
before_stepcallback: compute desired width/height or material from time or other logic. - Call
robot.update_rod_geometry(width=w, height=h)and/orrobot.update_rod_material(...). - Call
stepper.refresh_rod_params(robot)so the analytical energy model (e.g. Sano) receives updated parameters. - Run the step as usual.
The shear_induced_homotopy/ directory demonstrates homotopy on top of the shear-induced bifurcation setup:
- Phases 1–2: Apply compression and shear as above.
- Phase 3 (homotopy): No boundary motion; ramp rod width from
start_widthtoend_widthusingupdate_rod_geometry(width=...)andrefresh_rod_params()at each step. - Phase 4: Reverse shear with the new width.
The homotopy phase smoothly interpolates cross-section width during the simulation, enabling path-following and bifurcation studies.
cd shear_induced_homotopy
python simulate.py --config simulate_shear_homotopy_config.yaml --pkl-dir out/pkls --plot-dir out/plots
# Override discretization or energy model
python simulate.py --config simulate_shear_homotopy_config.yaml --pkl-dir out/pkls --plot-dir out/plots --nodes 45
python simulate.py --config simulate_shear_homotopy_config.yaml --pkl-dir out/pkls --plot-dir out/plots --energy-model sano
# Plot Hmid vs delta-W (with FEA reference and shear force)
python plot_shear_homotopy.py --config config_shear_homotopy_plot.yaml --plot-dir out/plots --output shear_homotopy_sano.png| Section | Parameter | Description |
|---|---|---|
homotopy |
start_time, end_time |
Time window for width ramp (no boundary motion) |
homotopy |
start_width, end_width |
Width at start/end of homotopy (null = use initial) |
The before_step callback applies gravity ramp, motion phases, and during the homotopy window calls robot.update_rod_geometry(width=...) and stepper.refresh_rod_params(robot).
- 3D discrete elastic rod stretching, bending, and twisting.
- 3D discrete elastic shell hinge and mid-edge bending.
- Adaptive implicit Euler with robust linear solve (Tikhonov regularization, SVD fallback).
- Generalized elastic energy framework with pluggable analytical models (Kirchhoff, Sadowsky, Wunderlich, Sano, Audoly).
- Homotopic updates: change rod width, height, and material during simulation.
- Implicit integration schemes (Euler, Newmark-beta).
- Dense and sparse (PyPardiso) solvers.
