RasmahRasmah

Getting Started: Differentiable Geometry

println("Julia ", VERSION)
Julia 1.12.6

Overview

This first tutorial introduces Rasmah through a real part — a Ø12.7 mm bearing ball, the half-inch ball of a standard bearing — and shows the whole idea of the system on it: build the shape, sample its signed-distance field, measure it with exact formulas, differentiate through the pipeline, and see it. Dimensions are given in millimetres; Rasmah stores lengths in SI, so the code writes them as metres (12.7 mm = 0.0127 m).

using Rasmah

# Geometry is stored in SI (metres); these helpers render millimetres.
mm(v)  = round(uconvert(u"mm", v); digits = 1)
mm3(v) = round(uconvert(u"mm^3", v); digits = 0)
mm3 (generic function with 1 method)

What is Rasmah?

Rasmah is a differentiable, programmatic CAD (computer-aided design) framework written in Julia. It lets you build geometry from features (spheres, boxes, cylinders, cones, planes, extrusions, …), work with that geometry as a signed-distance field, an exact boundary representation, or a mesh, simulate it with the finite-element method, and — crucially — differentiate every quantity through the entire pipeline with respect to the model’s continuous parameters.

The mental model is simple:

A CAD model is a differentiable program. Its continuous parameters (radii, positions, sizes, …) influence geometry, meshing, and simulation, while discrete topology is tracked explicitly.

This first tutorial stays purely geometric — no finite elements. You will build a bearing ball, sample its field, compute exact and approximate quantities, take derivatives, and see the shape.

Signed distance fields

Rasmah represents implicit geometry with a signed distance field (SDF). An SDF is a function f(x) that returns:

  • a negative value inside the shape,
  • a positive value outside, and
  • zero exactly on the boundary,

with |f(x)| equal to the Euclidean distance to the boundary (at least near the surface).

Let’s build the bearing ball — a sphere of radius 6.35 mm — and sample its field at a few points:

r_ball = 0.00635                                  # Ø12.7 mm = ½-inch bearing ball
ball = sphere(r_ball)
Sphere{Float64}(0.00635)

A shape is a small immutable description — sphere(r_ball) here — and every shape answers field queries directly through field:

println("at the origin:   ", field(ball, [0.0, 0.0, 0.0]))          # inside  → negative
println("on the surface:  ", field(ball, [r_ball, 0.0, 0.0]))       # boundary → 0
println("two radii away:  ", field(ball, [2 * r_ball, 0.0, 0.0]))   # outside → positive
at the origin:   -0.00635
on the surface:  0.0
two radii away:  0.00635

Exact quantities

For primitive shapes Rasmah provides analytic formulas that are far more accurate than any mesh-based estimate. volume and bounds work directly on shapes:

println("ball volume = ", mm3(volume(ball)), " mm³")     # 4/3 π r³
println("analytic    = ", round(4π / 3 * r_ball^3 * 1e9; digits = 0), " mm³")
println("bounds      = ", mm.(bounds(ball)[1]), " .. ", mm.(bounds(ball)[2]), " mm")
ball volume = 1073.0 mm³
analytic    = 1073.0 mm³
bounds      = [-6.4, -6.4, -6.4] .. [6.4, 6.4, 6.4] mm

volume also works on cylinders and boxes, with the obvious formulas — a Ø40 × 80 mm piston and a 10 × 20 × 30 mm stock block:

println("piston volume = ", mm3(volume(cylinder(0.020, 0.080))), " mm³")  # π r² h
println("block volume  = ", mm3(volume(box(0.010, 0.020, 0.030))), " mm³") # w·l·h
piston volume = 100531.0 mm³
block volume  = 6000.0 mm³

Gradients

There are two kinds of gradient in Rasmah, and it is worth keeping them apart.

Gradient of a field (spatial)

gradient(shape, x) returns the gradient of the field with respect to the point x — the direction in which the field increases fastest. For an exact SDF this is the outward unit normal at the boundary:

gradient(ball, [0.0, 0.0, r_ball])   # points along +z, exactly at the surface
3-element Vector{Float64}:
 0.0
 0.0
 1.0

Derivatives with respect to parameters

For a scalar function of a scalar parameter, derivative(f, x) gives df/dx. For a function of several parameters, gradient(f, [x, y, …]) returns the gradient vector. Both run through automatic differentiation (forward mode by default).

A classic sanity check: the surface area of a sphere is A(r) = 4π r², so dA/dr = 8π r. We obtain A(r) numerically — by meshing the sphere — and differentiate straight through the mesh:

area(r) = surface_area(surface_mesh(sphere(r)))

println("dA/dr at r = 6.35 mm = ", round(ustrip(derivative(area, r_ball)) * 1000; digits = 2), " mm")
println("analytic 8π r         = ", round(8π * r_ball * 1000; digits = 2), " mm")
dA/dr at r = 6.35 mm = 158.83 mm
analytic 8π r         = 159.59 mm

The derivative flows through geometry → meshing → measurement automatically, even though meshing is a discrete operation (the mesh topology is fixed during differentiation, so the derivative is well defined). The mesh estimate lands within half a percent of the exact 8π r ≈ 159.6 mm.

Analytic formulas are differentiable too, and multi-parameter gradients are taken with a vector of parameters — here the piston again, V(r, h) = π r² h:

# V(r, h) = π r² h  ⇒  ∇V = (2π r h, π r²)
rv, hv = 0.020, 0.080
println(gradient(x -> volume(cylinder(x[1], x[2])), [rv, hv]))
println("analytic:  ", [2π * rv * hv, π * rv^2])
[0.01005309649148734 m^3, 0.0012566370614359172 m^3]
analytic:  [0.01005309649148734, 0.0012566370614359172]

Visualizing

The same shape can be turned into a surface mesh and shown as an interactive 3D viewer right in the notebook:

vtk(surface_mesh(ball))

vtk works on surface meshes, volume meshes, BReps, and shapes alike, and renders them inline (drag to rotate, scroll to zoom).

Next steps