Materials

One object for a part’s optical and physical properties, read by the renderer and the solver.

A Material carries a part’s optical properties, which the renderer shades with, and its physical properties, which the solver reads in SI units. This page covers stating them, the catalogue of sourced values, free properties, the material field of a multi-material scene, and mass.

Example

import jax.numpy as jnp
from cadjoint.materials import aluminium_6061, copper_c11000
from cadjoint.sdf.boolean import Union
from cadjoint.sdf.primitives import Box, Cylinder
from cadjoint.sdf.transforms import Translate

sink = Box([0.6, 0.6, 0.2], material=aluminium_6061())
slug = Translate(Cylinder(radius=0.2, height=0.4, material=copper_c11000()),
                 offset=jnp.array([0.0, 0.0, 0.1]))
scene = Union((sink, slug), smoothness=0.03)

print(slug.material_at(jnp.array([0.0, 0.0, 0.1])).get("conductivity"))  # 391.0

Properties

A Material has two families of properties:

  • Optical — color, roughness, metallic, opacity, ior, reflectivity. What the renderer shades with. Every one has a default, so a bare Material() is white matte plastic.
  • Physical — density, conductivity, specific_heat, youngs_modulus, poisson_ratio, thermal_expansion, yield_strength. What the solver solves with, in SI units. Every one defaults to None, meaning not specified.

Both families live in one object, so a scene states a material once: a slug declared as copper is orange, conducts 391 W/(m·K) and has a density of 8940 kg/m³.

from cadjoint.render import Material

copper = Material(
    name="copper",
    color=[0.72, 0.45, 0.20], roughness=0.30, metallic=1.0,
    density=8940.0, conductivity=391.0, specific_heat=385.0,
    youngs_modulus=117e9, poisson_ratio=0.34,
    thermal_expansion=17.0e-6, yield_strength=69e6,
)

Unspecified properties

A physical property that is not stated has no default value.

Material(color=[1.0, 0.0, 0.0]).get("density")   # None

A study that needs a missing property fails and names it:

ValueError: study 'sink-conduction': the scene's material field does not
specify 'conductivity' for 6412 of 6412 elements. Give every material in the
simulated domain a 'conductivity' value (see cadjoint.materials for a
catalogue of real ones), or pass an explicit scalar to the study.

Internally an unstated property is NaN in as_dict(), so the material dict keeps one static pytree structure across a scene, and blending an unstated value with a stated one yields NaN.

Catalogue

cadjoint.materials ships sourced values. Each entry is a factory, not a singleton, so marking one scene’s copper free does not affect another scene. The example builds a scene from two of them.

Factory Density (kg/m³) k (W/(m·K)) cₚ (J/(kg·K)) E (GPa) ν α (1/K) Yield (MPa)
aluminium_6061() 2700 167 896 68.9 0.33 23.6e-6 276
copper_c11000() 8940 391 385 117 0.34 17.0e-6 69
steel_1018() 7870 51.9 486 205 0.29 11.5e-6 370
titanium_ti6al4v() 4430 6.7 526.3 113.8 0.342 8.6e-6 880
fr4() 1850 0.29 1100 24 0.136 14e-6 310
silicon() 2329 148 700 130 0.28 2.6e-6 165
thermal_pad() 2500 3.0 1000 0.001 0.49 200e-6 0.5
pla() 1240 0.13 1800 3.5 0.36 68e-6 50

Every factory’s docstring cites its sources — MatWeb and ASM datasheets for the metals, the CRC Handbook for the elements, NEMA/IPC and laminate datasheets for FR-4, Hopcroft et al. (JMEMS 2010) for silicon’s modulus, NatureWorks and Farah et al. for PLA.

Three entries have caveats, also stated in their docstrings:

  • FR-4 is orthotropic. The entry takes the through-thickness conductivity (0.29, versus ~0.8–1.0 in plane) and the in-plane elastic constants, the values that usually govern the result. yield_strength holds the ultimate tensile strength — FR-4 is brittle and has no yield plateau.
  • Silicon is elastically anisotropic; the entry uses ⟨100⟩ (E = 130 GPa). Its yield_strength is a conservative practical fracture strength, which in reality is dominated by edge and surface damage.
  • The thermal pad has ν = 0.49. Elastomers are nearly incompressible, and linear HEX8/TET4 elements lock volumetrically there. Use TET10, or lower the ratio, if the pad’s compliance matters.

Free properties

Physical properties use the same Parameter containers as colors, so an optimizer can tune them:

tunable = Material("tim", conductivity=3.0, free=True)
tunable.params["conductivity"].free     # True
tunable.params["conductivity"].bounds   # (0.001, 3000.0)

free=True marks only the properties the material states. The bounds are wide engineering brackets (aerogel to tungsten) that keep a descent physical; they do not encode a material choice.

Material fields

Every SDF node answers material_at(p), and the smooth booleans blend the answers. A scene built from several materials is therefore a continuous field of physical properties: copper’s conductivity inside the slug, aluminium’s outside, and a transition as wide as the CSG blend between them.

The simulation layer samples this field per element at solve time; see material properties. Moving the interface moves the property field smoothly, so the gradient flows through it.

Mass

With densities on the materials, mass is computable and differentiable, so it can regularize an optimization:

from cadjoint.sdf import material_mass, volume

lattice = dict(bounds=(-0.7, -0.7, -0.3), size=(1.4, 1.4, 0.8), resolution=70)
print(float(volume(scene, **lattice)))          # 0.609  m³, shape only
print(float(material_mass(scene, **lattice)))   # 2074.6 kg, weighted by what each region is

material_mass takes the same arguments as volume: a sampling box, a resolution, and the epsilon of the sigmoid that softens the boundary. It also accepts an explicit sample lattice, the form a scene’s regularizer usually has:

import jax
from cadjoint import extract_parameters, functionalize

free, fixed, _ = extract_parameters(scene)
sdf = functionalize(scene)(free, fixed)
axis = jnp.linspace(-0.7, 0.7, 70)
cells = jnp.stack(jnp.meshgrid(axis, axis, axis, indexing="ij"), -1).reshape(-1, 3)
cell_volume = float(axis[1] - axis[0]) ** 3

# a volume regularizer …
cell_volume * jnp.sum(jax.nn.sigmoid(-jax.vmap(sdf)(cells) / 0.03))
# … and the same thing weighted by what each cell is made of
material_mass(scene, cells, cell_volume, epsilon=0.03)

For a single-material part the two differ by a constant factor; for a design that trades copper for aluminium only mass is meaningful. On a mesh, cadjoint.fem.properties.total_mass(points, cells, density) gives the exact element-wise sum instead of a lattice estimate, and a solved study reports it as result.mass.

Inspection

describe() returns the JSON payload the playground’s Materials window reads and edits, physical properties included, as source rewrites:

Material("al", color=[0.8, 0.8, 0.82], density=2700.0, conductivity=167.0).describe()
{
  "color": [0.8, 0.8, 0.82],
  "roughness": 0.5, "metallic": 0.0, "opacity": 1.0,
  "ior": 1.0, "reflectivity": 0.0,
  "physical": {
    "density": 2700.0, "conductivity": 167.0,
    "specific_heat": null, "youngs_modulus": null,
    "poisson_ratio": null, "thermal_expansion": null,
    "yield_strength": null
  },
  "units": {"density": "kg/m^3", "conductivity": "W/(m*K)", "…": "…"},
  "free": {"color": false, "density": false, "…": false}
}

physical values are floats or null; units names the SI unit of each so a panel can label a field without hard-coding it; free flags which properties an optimizer controls.

API