cadjoint

cadjoint is differentiable code-first CAD. Sketches, constraints, SDF geometry, meshing and finite-element simulation compose into one function JAX can differentiate end to end.

Try it in the browser

The shipped scenes with their free parameters, construction handles, wireframe and STL export. It runs entirely in the browser, with nothing to install: WebGPU draws the model, and meshing falls back to WebAssembly without it.

Warning

The API is not stable. Expect breaking changes.

The cadjoint playground on the Model desk: scene.py in the editor on the left, the parametric heat sink in the viewport with its blue sketch handles, and the object tree, properties and materials windows docked on the right

The playground runs the whole chain in the browser (modelling, meshing, studies and optimization) and writes every edit back into the Python source.

pip install cadjoint installs the library. Getting started covers installation, supported platforms and the playground command; the playground guide covers the app.

Components

Layer What it does
Geometry Parametric Vector / Scalar carrying free / fixed flags
SDF Primitives, booleans and transforms, all differentiable
Sketches 2D profiles on work planes, extruded or revolved into solids that share their parameters
Constraints Geometric relationships that reduce DOF, with Newton projection onto the constraint manifold
Functionalize Convert a parametric scene to a pure JAX function for jit / grad / vmap
Meshing Dual contouring built bottom-up in JAX; exact gradients through every continuous stage; OBJ / STL / STEP export
Simulation Declared SimMesh and thermal/elastic studies, node selections, and pluggable solver backends
Flow A prototype lattice-Boltzmann conjugate heat-transfer solver on the scene’s own field, with no mesh
Optimization Declarative descent over named CAD parameters with the simulation as objective
Plugins Meshers and solvers behind one apply/vjp contract, so one jax.grad crosses every tool boundary
Rendering Forward sphere tracing, plus StableHLO → WGSL shader compilation
The playground The whole toolchain as a browser app, with the Python source as its only state

Pipeline

CAD parameters θ  ──►  constraints ──► SDF        (JAX autodiff)
        │                              │
        │                              ▼
        │                  dual-contoured surface   (JAX autodiff:
        │                              │             Newton on the true SDF)
        │                              ▼
        │              ┌─ tetfill Tesseract ─────┐ (TetGen; frozen topology,
        │              │  surface → TET4/TET10   │  exact pass-through VJP)
        │              └───────────┬─────────────┘
        │                          ▼
        │              ┌─ solver Tesseract ──────┐ (jax-fem adjoint, or
        │              │  thermal / elastic FEM  │  CalculiX Fortran adjoint)
        │              └───────────┬─────────────┘
        │                          ▼
        └──────────  ∂J/∂θ  ◄──  objective J      (JAX autodiff)

No two stages compute their derivative the same way, and TetGen has none; the frozen-topology pass-through supplies it. On the parametric L-bracket the composed loop lowers the objective 73% in 30 steps, with the adjoint checked against finite differences at every boundary.

Quick start

Three distance constraints determine an unknown point; the solved scene is an ordinary JAX function.

import jax.numpy as jnp
from cadjoint import extract_parameters, functionalize
from cadjoint.constraints import DistanceConstraint, solve_constraints
from cadjoint.geometry.parameters import Vector
from cadjoint.sdf.primitives import Sphere
from cadjoint.sdf.transforms import Translate

# Unknown point — wrong initial guess
p = Vector(jnp.array([0.5, 0.5, 0.0]), free=True, name="p")
scene = Translate(Sphere(radius=0.5), offset=p)

# Fixed anchors
anchor_a = Vector(jnp.array([0.0, 0.0, 0.0]), free=False, name="a")
anchor_b = Vector(jnp.array([4.0, 0.0, 0.0]), free=False, name="b")
anchor_c = Vector(jnp.array([2.0, 3.0, 0.0]), free=False, name="c")
true_p = jnp.array([2.0, 1.0, 0.0])

for anchor in (anchor_a, anchor_b, anchor_c):
    DistanceConstraint(p, anchor, float(jnp.linalg.norm(true_p - anchor.value)))

solved = solve_constraints(scene)
_, fixed, _ = extract_parameters(scene)
sdf = functionalize(scene)(solved, fixed)

print(sdf(true_p))  # ≈ -0.5 — the solved sphere is centred on the true point

Modelling covers constraints and degrees of freedom; the guides continue through meshing, simulation and optimization.