Auxetic lattices are the structures that get wider when you stretch them — the opposite of ordinary materials. That property makes them excellent for cushioning, prosthetic liners, lightweight panels and things that need to conform under load. The good news: the two most useful families (gyroid and diamond, both Triply Periodic Minimal Surfaces) fit in about 30 lines of Python, and print fine on a stock FDM machine.
Here's how to generate them, and the density trap that most generators don't tell you about.
The math is one formula per lattice
A TPMS is defined by an implicit equation f(x, y, z) = t. Sweep t and you get nested surfaces through the volume. The two workhorses:
import numpy as np
def gyroid_field(X, Y, Z):
return (np.sin(X) * np.cos(Y) +
np.sin(Y) * np.cos(Z) +
np.sin(Z) * np.cos(X))
def diamond_field(X, Y, Z):
return (np.cos(X) * np.cos(Y) +
np.cos(Y) * np.cos(Z) +
np.cos(Z) * np.cos(X))
The solid is everything where f < t (or > t, depending on which phase you want).
From field to printable STL: marching cubes on a padded grid
from skimage import measure
import trimesh
def lattice_to_mesh(field_fn, size_mm=30.0, n=96, t=0.0):
xs = np.linspace(-np.pi * 2, np.pi * 2, n)
X, Y, Z = np.meshgrid(xs, xs, xs, indexing="ij")
solid = (field_fn(X, Y, Z) < t).astype(float)
# zero-pad: marching cubes MUST see closed boundaries or your mesh leaks
padded = np.pad(solid, 1)
spacing = size_mm / n
verts, faces, _, _ = measure.marching_cubes(padded, level=0.5, spacing=(spacing,) * 3)
mesh = trimesh.Trimesh(vertices=verts, faces=faces, process=False)
if mesh.volume < 0:
mesh.invert()
return mesh
Two details that decide whether the print works:
- Pad before marching cubes. Without the pad, surfaces touching the array edge stay open — the slicer sees a leaky shell and the print fails in ways that waste hours.
-
Run
fix_normalswithmultibody=True. Depending ont, a TPMS has disconnected solid regions; a single-body normal fix flips the wrong walls.
The density trap: target ≠ measured
You'd think "set t so 50% of the voxels are solid, get 50% density." You don't. The isosurface cuts through the boundary voxels, and the faceted surface carves away material, so the printed mesh measures noticeably lighter than the voxel target.
I measured this on gyroid at three target fractions:
| target | measured (mesh volume) |
|---|---|
| 0.30 | ~0.27 |
| 0.50 | ~0.43 |
| 0.70 | ~0.64 |
Roughly a 3–15% loss depending on density — monotonic and predictable, which means you can calibrate: pick the target that lands where you actually want. If a generator reports only the target fraction, it's hiding the number that matters. Measure the mesh volume, not the voxel count.
(t isn't linear in density either — I pick it from the voxel histogram, scanning candidate levels until the histogram fraction matches the target.)
What to print first
A 30 mm cube of gyroid at measured ~0.43 is a good first test: prints without supports (the surfaces self-support at shallow angles almost everywhere), and you can feel the auxetic behavior by pulling opposite faces. Diamond at 0.3 is the stiff-but-light option.
If you want the pipeline without assembling it — level calibration, both lattices, Blender import, and measured-vs-target density reporting per part — that's my LatticeGen plugin (includes a Blender bridge), and PrintIQ adds bed packing and slicer time estimates on top. The pre-print mesh checks behind them are free in stl-precheck.
Start at 0.3–0.45 measured density: that's the sweet spot where auxetic behavior shows up without needing support material.
Top comments (0)