Skip to content

First Optimization

This tutorial walks through a mold direction optimization: finding the best pull direction for an injection-molded part to minimize undercuts. BRepAX provides a differentiable undercut metric, enabling gradient descent on the unit sphere.

Setup

import jax
import jax.numpy as jnp

jax.config.update("jax_enable_x64", True)

from brepax.experimental.applications.mold_direction import (
    optimize_mold_direction,
    undercut_volume,
)
from brepax.primitives import Box, Cylinder

Define a Composite Shape

Build an L-bracket by subtracting a notch and a through-hole from a body box. The CSG tree uses max/min on SDF values, and every operation is differentiable.

body = Box(
    center=jnp.array([0.0, 0.0, 0.0]),
    half_extents=jnp.array([1.5, 1.0, 1.0]),
)
notch = Box(
    center=jnp.array([1.0, 0.0, 0.5]),
    half_extents=jnp.array([0.6, 1.1, 0.6]),
)
hole = Cylinder(
    point=jnp.array([0.0, 0.0, 0.0]),
    axis=jnp.array([0.0, 1.0, 0.0]),
    radius=jnp.array(0.3),
)

def bracket_sdf(x):
    """L-bracket with through-hole: body - notch - hole."""
    return jnp.maximum(jnp.maximum(body.sdf(x), -notch.sdf(x)), -hole.sdf(x))

Compare Pull Directions

The undercut metric evaluates how much surface area opposes a given pull direction. Lower values mean easier mold release.

lo = jnp.array([-2.5, -2.0, -2.0])
hi = jnp.array([2.5, 2.0, 2.0])

for name, d in [("+z", jnp.array([0., 0., 1.])),
                ("-z", jnp.array([0., 0., -1.]))]:
    uc = undercut_volume(bracket_sdf, d, lo=lo, hi=hi, resolution=48)
    print(f"{name}: undercut = {float(uc):.4f}")

Run the Optimizer

Start from a sub-optimal direction (into the notch) and let projected gradient descent on the sphere find a better one.

result = optimize_mold_direction(
    bracket_sdf,
    initial_direction=jnp.array([1.0, 0.0, 1.0]),
    lo=lo,
    hi=hi,
    resolution=48,
    steps=200,
    lr=0.02,
)

Interpret Results

d_init = jnp.array([1.0, 0.0, 1.0]) / jnp.sqrt(2.0)
d_final = result.direction

uc_initial = float(undercut_volume(bracket_sdf, d_init, lo=lo, hi=hi, resolution=48))
uc_final = float(undercut_volume(bracket_sdf, d_final, lo=lo, hi=hi, resolution=48))
reduction = (uc_initial - uc_final) / uc_initial

print(f"Initial undercut: {uc_initial:.4f}")
print(f"Final undercut:   {uc_final:.4f}")
print(f"Reduction:        {reduction:.1%}")   # ~22% reduction
print(f"Converged:        {result.converged}")

The optimizer rotates the pull direction away from the notch, reducing the undercut by approximately 22%. The gradient flows through the SDF, through the Boolean subtract, through the surface integral, and back to the direction vector on the sphere.

Next Steps