Solvers

One section per solver: its method and backend, what it took to run it in Mosaic, its checks on each setup, and how to call it from your own code. Solvers that share an input and output schema form a domain.

Effort levels

Every solver has an intervention record that lists everything done to run it beyond using the library as intended, with the reason and evidence for each item. Each intervention gets a level by its mechanism, and a solver’s effort on a setup is the highest level among the interventions it requires there.

Level Mechanism Example
L0 Nothing beyond expected tweaks, meaning the public API with its defaults or documented options Setting a documented solver tolerance
L1 Undocumented configuration, found by reading the source An option that only the library’s source code mentions
L2 User code on the library’s public primitives A boundary condition or VJP rule that the library does not provide
L3 Changes to the solver itself A monkeypatch, a private API or a patched source file

What counts as intended use depends on the solver’s abstraction level:

  • application: running a program as documented.
  • framework: expressing the setup through the library’s API.
  • kernel-toolkit: building on the library’s primitives.

The record also lists deviations, the places where a solver runs a substitute for part of the setup, with their effect on the results. An upstream fix or a better configuration can lower a solver’s effort, so contributions are welcome on every item.

All solvers

Domain Solver Discretization Backend AD strategy GPU
Navier–Stokes (grid) Exponax Spectral jax autodiff ✓
INS.jl Finite volumes (FV) julia autodiff ✓
jax-cfd Finite differences (FD) jax autodiff ✓
OpenFOAM Finite volumes (FV) cpp none (forward only)
PhiFlow Finite differences (FD) jax hybrid ✓
PICT Finite volumes (FV) pytorch autodiff ✓
Warp-NS Finite differences (FD) warp autodiff ✓
XLB Lattice Boltzmann (LBM) jax autodiff ✓
Structural mechanics deal.II Finite elements (FE) cpp none (forward only)
FEniCS Finite elements (FE) fenics adjoint
Firedrake Finite elements (FE) firedrake adjoint
JAX-FEM Finite elements (FE) jax hybrid ✓
TopOpt.jl Finite elements (FE) julia adjoint
torch-fem Finite elements (FE) pytorch hybrid ✓
Heat transfer deal.II Finite elements (FE) cpp none (forward only)
FEniCS Finite elements (FE) fenics adjoint
Firedrake Finite elements (FE) firedrake adjoint
JAX-FEM Finite elements (FE) jax hybrid ✓
torch-fem Finite elements (FE) pytorch hybrid ✓

Structural mechanics

The solvers in this domain share the schema in mosaic/mosaic_shared/problems/structural_mesh/schemas.py and are benchmarked on Linear-elastic cantilever.

deal.II

deal.II Q1 trilinear hexahedral FEM linear-elasticity solver via C++ subprocess (UMFPACK direct). Forward-only — reference implementation for cross-framework validation.

Interventions and deviations

Abstraction level: framework.

Solves the system directly with UMFPACK L0 choice tuned

Solves the assembled system with deal.II’s SparseDirectUMFPACK instead of the CG with SSOR preconditioning of the step-8 tutorial the assembly follows.

  • Linear-elastic cantilever (too slow): With the tutorial’s SSOR-preconditioned CG the solver runs 1.7 times slower at nx = 128, with the same compliance.
Details
  • Linear-elastic cantilever: Evidence: #271, notes.
  • Record id: umfpack-direct-solve

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Not supported Not supported

Excluded experiments

  • cost/vjp_cost and gradient (not supported): The deal.II structural tesseract is forward-only. It exposes no AD path (no VJP endpoint, no differentiable inputs), so cost/vjp and gradient experiments are categorically out of scope.
  • optimization (not supported): The deal.II structural tesseract has no AD path, and topology optimization (compliance gradient-based ρ updates) requires a differentiable compliance output.

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/dealii-structural builds the image dealii_structural_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
E_max float Young’s modulus of the fully solid material [MPa].
nu float Poisson’s ratio.
xmin float Void stiffness ratio (E_min = xmin * E_max).
penal float SIMP penalisation exponent (E(ρ) = E_min + (E_max−E_min)·ρ^penal).

Outputs

Field Type ∂ Description
compliance Array[(), Float32] Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

FEniCS

FEniCS/DOLFIN CG1 FEM linear-elasticity solver with SIMP penalisation. Gradient ∂C/∂ρ via dolfin-adjoint tape replay (discrete adjoint derived at the variational level via UFL).

Interventions and deviations

Abstraction level: framework.

Solves the forward system with MUMPS L0 choice tuned

Passes solver_parameters={“linear_solver”: “mumps”} to the forward solve instead of using DOLFIN’s default LU backend (UMFPACK).

  • Linear-elastic cantilever (too slow): With UMFPACK, apply at nx = 128 is about 1.15 times slower, 10.1 s against 8.8 s.
Details
  • Linear-elastic cantilever: The times are from before and after the change that introduced it. Instrumentation put 99.6% of apply in the linear solve, so the gain is attributed to the solver switch. No failure without it is recorded on this setup. Evidence: #182, section 2 (measured together with tape replay).
  • Record id: mumps-forward-solver

Patches dolfin-adjoint to solve the adjoint with MUMPS L3 choice

Replaces dolfin-adjoint’s linear solve at import time with one that always uses MUMPS. Legacy dolfin-adjoint does not pass the forward solver_parameters to the adjoint solve, which then runs UMFPACK.

  • Linear-elastic cantilever (too slow): Without the patch the VJP at nx = 128 takes 14.6 s against 11.1 s, with identical gradients.
Details
  • Linear-elastic cantilever: At nx = 64 the VJP takes 4.7 s against 3.2 s. Small meshes show no difference. The patched function is fenics_adjoint.types.compat.linalg_solve. Evidence: A/B on an A100, 2026-10-26, in the intervention cleanup notes (item 3), not yet published.
  • Upstream: Not filed: legacy dolfin-adjoint has had no release since 2019.1.0. Per #182, newer dolfin-adjoint passes linear_solver to the adjoint solve.
  • Record id: mumps-adjoint-monkeypatch

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/fenics-structural builds the image fenics_structural_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
E_max float Young’s modulus of the fully solid material.
nu float Poisson’s ratio.
xmin float Void stiffness ratio (E_min = xmin * E_max).
penal float SIMP penalisation exponent (E(rho) = E_min + (E_max-E_min)*rho^penal).

Outputs

Field Type ∂ Description
compliance Array[(), Float32] ✓ Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

Firedrake

Firedrake CG1 FEM linear-elasticity solver with SIMP penalisation. Gradient ∂C/∂ρ via firedrake-adjoint (pyadjoint ReducedFunctional) — an independent tape-based discrete adjoint complement to FEniCS.

Interventions and deviations

Abstraction level: framework.

No interventions are recorded.

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/firedrake-structural builds the image firedrake_structural_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
E_max float Young’s modulus of the fully solid material.
nu float Poisson’s ratio.
xmin float Void stiffness ratio (E_min = xmin * E_max).
penal float SIMP penalisation exponent (E(rho) = E_min + (E_max-E_min)*rho^penal).

Outputs

Field Type ∂ Description
compliance Array[(), Float32] ✓ Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

JAX-FEM

JAX-FEM HEX8 linear-elasticity solver with SIMP stiffness E(ρ) = E_min + (E_max − E_min)·ρ^penal. Gradient ∂C/∂ρ via JAX automatic differentiation through the linear FE solve (hybrid implicit-function-theorem adjoint).

Interventions and deviations

Abstraction level: framework.

Compiles JAX-FEM’s BiCGSTAB solve once per matrix size L2 choice tuned

Passes JAX-FEM’s own Jacobi-preconditioned BiCGSTAB, compiled once per matrix size, to ad_wrapper through the custom_solver option for the forward and adjoint solves.

  • Linear-elastic cantilever (too slow): The default jax_solver recompiles on every call, and the umfpack_solver option used before is about 8 times slower at N = 128 (11.6 s against 1.49 s).
Details
  • Linear-elastic cantilever: The custom solver uses the default jax_solver’s algorithm and tolerances. The default jax_solver runs bicgstab eagerly, so it compiles 2 XLA programs per forward call and 4 per VJP, a fixed overhead on every call. The umfpack_solver option gives identical outputs but is much slower on large meshes. At N = 128, spatial_cost took 11.6 s with umfpack_solver and 1.49 s with this solver, vjp_cost/by_N 23.5 s and 2.91 s. Evidence: #286, before/after table; comment above _bicgstab.
  • Record id: jitted-bicgstab-solver

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/jax-fem builds the image jax_fem_structural_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.

Outputs

Field Type ∂ Description
compliance Array[(), Float32] ✓ Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

TopOpt.jl

TopOpt.jl HEX8 FEM linear-elasticity solver with SIMP penalisation, Julia + Ferrite.jl backend. Gradient ∂C/∂ρ via analytical SIMP adjoint sensitivity computed alongside the forward solve.

Interventions and deviations

Abstraction level: framework.

No interventions are recorded.

Setup deviations

  • Applies the cantilever load as nodal forces (Linear-elastic cantilever): The harness gives TopOpt.jl the cantilever load as nodal forces (CLOAD) instead of a traction: the shares that integrating the traction over the bilinear face elements gives each node. Effect: None: on the uniform mesh the nodal forces equal the integrated traction. Evidence: TopOpt.jl 0.14.0’s INP reader takes surface loads only as a normal pressure (DLOAD P), and the cantilever load is tangential; _traction_solvers in mosaic/benchmarks/problems/structural_mesh/physics.py.

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/topopt-jl builds the image topopt_jl_structural_grid. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
E float Young’s modulus of the solid material.
nu float Poisson’s ratio.
xmin float Minimum density (void stiffness) to prevent singular stiffness matrix.

Outputs

Field Type ∂ Description
compliance Array[(), Float32] ✓ Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

torch-fem

torch-fem Solid HEX8 linear-elasticity solver with SIMP stiffness E(ρ) = E_min + (E_max − E_min)·ρ^p — the PyTorch backend in structural-mesh. Gradient ∂C/∂ρ via PyTorch autograd through the FE solve (implicit-function-theorem adjoint); GPU-native.

Interventions and deviations

Abstraction level: framework.

Runs the solver on the GPU L0 choice

With a GPU present, runs torch-fem on it instead of torch’s default CPU device.

  • Linear-elastic cantilever: No comparison with the CPU exists.
Details
  • Linear-elastic cantilever: The tesseract has run on the GPU since its first version.
  • Record id: gpu-device

Sets torch’s default device and dtype globally L0 required

Sets torch’s default device and dtype (float64) process-wide, so the tensors torch-fem allocates internally land on the GPU with the mesh, material and load tensors.

  • Linear-elastic cantilever (crash): torch-fem allocates auxiliary tensors without device=, so they land on the CPU and collide with the CUDA tensors.
Details
  • Linear-elastic cantilever: No error log is recorded. Evidence: Code comment; torch-fem’s benchmarks/utils.py at v0.12.1 and its current installation docs set the default device the same way.
  • Record id: global-default-device

Solves with Jacobi-preconditioned CG on the GPU L0 choice

On the GPU, passes method=“cg” and preconditioner=“jacobi” to model.solve for the forward and adjoint solves. On the CPU torch-fem chooses.

  • Linear-elastic cantilever (too slow): torch-fem 0.12.1 solves systems below 10,000 DOFs directly with SciPy on the CPU, even when the matrix is on the GPU.
Details
  • Linear-elastic cantilever: Larger systems already get CG with Jacobi without AmgX, so the setting keeps the smaller meshes on the device. There is no isolated measurement. Evidence: #188; resolve_method and resolve_library in torch-fem v0.12.1 src/torchfem/sparse.py.
  • Record id: gpu-cg-jacobi

Loosens the CG stopping tolerance to 1e-6 L0 choice tuned

The GPU CG stops at a relative residual of 1e-6 (stol) instead of torch-fem’s 1e-10.

  • Linear-elastic cantilever (too slow): At torch-fem’s default of 1e-10, spatial_cost at N = 128 took 0.435 s against 0.338 s, with every status unchanged.
Details
  • Linear-elastic cantilever: vjp_cost took 0.717 s against 0.530 s. Measured on a shared GPU. Every status is unchanged from 1e-12 to 1e-6. Forward metrics are bit-identical and gradient checks move by less than 1e-9. Evidence: #301, notes.
  • Record id: cg-tolerance

Makes the CUDA assembly deterministic L0 choice

Enables torch.use_deterministic_algorithms(True, warn_only=True), so torch-fem’s CUDA scatter-adds in assembly sum in a fixed order.

  • Linear-elastic cantilever: Run-to-run differences were shown on the heat setup only.
Details
  • Linear-elastic cantilever: On this setup the setting is kept for consistency, with no measurable cost. Evidence: #200, notes.
  • Record id: deterministic-kernels

Setups and checks

Setup Effort Forward Gradient Optimization
Linear-elastic cantilever L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/structural-mesh/torch-fem-structural builds the image torch_fem_structural_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. Values in [0, 1]: 0 = void, 1 = solid.
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe displacement (values shape (n_groups, 3), None → zero); neumann.mask/values prescribe surface traction (values shape (n_groups, 3)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
E_max float Young’s modulus of fully solid material (ρ = 1).
nu float Poisson’s ratio (density-independent).
xmin float Void stiffness ratio E_min / E_max.
p_exp float SIMP penalisation exponent p in E(ρ) = E_min + (E_max−E_min)·ρ^p.

Outputs

Field Type ∂ Description
compliance Array[(), Float32] ✓ Structural compliance C = F^T U (scalar). Minimising compliance is equivalent to maximising stiffness.

Heat transfer

The solvers in this domain share the schema in mosaic/mosaic_shared/problems/thermal_mesh/schemas.py and are benchmarked on Steady conduction in a slab.

deal.II

deal.II Q1 trilinear hexahedral FEM heat-conduction solver with SIMP conductivity (C++ subprocess, AMG-preconditioned CG). Forward-only — reference implementation for cross-framework validation.

Interventions and deviations

Abstraction level: framework.

Preconditions the CG solve with algebraic multigrid L0 choice tuned

Preconditions the CG solve with Trilinos ML algebraic multigrid (PreconditionAMG, elliptic) instead of the SSOR of the step-5 tutorial the assembly follows.

  • Steady conduction in a slab (too slow): With the tutorial’s SSOR preconditioner the largest mesh solves about 2.4 times slower (61.1 s against 25.6 s), with the same forward results.
Details
  • Steady conduction in a slab: The times are spatial_cost at nx = 1024, where UMFPACK takes 35.6 s. Every forward metric is the same with SSOR. AMG is faster from nx = 256 on. At nx = 16 its setup makes it slower, 0.020 s against 0.014 s. Evidence: #305, notes table.
  • Record id: amg-preconditioner

Loosens the CG stopping tolerance to 1e-8 L0 choice tuned

The CG solve stops at a residual of 1e-8 times the right-hand-side norm, with a cap of 50000 iterations.

  • Steady conduction in a slab (too slow): At the earlier tolerance of 1e-12 the largest mesh solves more slowly (60.1 s against 51.1 s), with identical forward metrics.
Details
  • Steady conduction in a slab: The times are spatial_cost at nx = 1024, measured with SSOR. With AMG, #305 checked only that the forward metrics are unchanged at 1e-8. Evidence: #266, before/after table; #305, notes.
  • Record id: cg-tolerance

Setups and checks

Setup Effort Forward Gradient Optimization
Steady conduction in a slab L0 Pass Not supported Not supported

Excluded experiments

  • cost/vjp_cost and gradient (not supported): The deal.II thermal tesseract is forward-only. It exposes no AD path (no VJP endpoint, no differentiable inputs), so cost/vjp and gradient experiments are categorically out of scope.
  • optimization (not supported): The deal.II thermal tesseract exposes no AD path, and conductivity recovery (gradient-based ρ updates that minimize the misfit to a target temperature field) requires a differentiable identification-error output.

Use it in your code

tesseract build mosaic/tesseracts/thermal-mesh/dealii-heat builds the image dealii_heat_thermal_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. ρ=1 → fully conducting, ρ=0 → insulating.
source Array[(None,), Float32] Per-element volumetric heat source (W/m³), shape (n_cells,). Active slice = source[:hex_mesh.n_faces]. Zero by default (topology-optimisation mode; no body heat source).
target_temperature Array[(None,), Float32] Per-node target temperature for the inverse (source-identification) objective, shape (n_nodes,). identification_error =
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe temperature (values shape (n_groups, 1), None → zero); neumann.mask/values prescribe heat flux (values shape (n_groups, 1)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
k_max float Maximum thermal conductivity (fully solid material).
p_exp float SIMP penalisation exponent p (k(ρ) = k_min + (k_max−k_min)·ρ^p).

Outputs

Field Type ∂ Description
thermal_compliance Array[(), Float32] Total thermal compliance C = ∮_Γ_N q_n · T dΓ (scalar). Equals the work done by the Neumann heat flux on the temperature field.
identification_error Array[(), Float32] Source-identification objective:

FEniCS

FEniCS P1 FEM heat-conduction solver with SIMP penalisation. Gradients (∂C/∂ρ, ∂C/∂source) via dolfin-adjoint tape replay.

Interventions and deviations

Abstraction level: framework.

Solves the forward system with MUMPS L0 required

Passes solver_parameters={“linear_solver”: “mumps”} to the forward solve instead of using DOLFIN’s default LU backend (UMFPACK).

  • Steady conduction in a slab (out of memory): With UMFPACK the forward solve runs out of memory at the largest cost-sweep size (nx = 1024).
Details
  • Steady conduction in a slab: This showed as an error on the results page. No peak memory was recorded. Evidence: https://github.com/pasteurlabs/mosaic/issues/180 (comment by the FEniCS developer who proposed the change); merged in #182.
  • Record id: mumps-forward-solver

Patches dolfin-adjoint to solve the adjoint with MUMPS L3 required

Replaces dolfin-adjoint’s linear solve at import time with one that always uses MUMPS. Legacy dolfin-adjoint does not pass the forward solver_parameters to the adjoint solve, which then runs UMFPACK.

  • Steady conduction in a slab (out of memory): With MUMPS in the forward solve only, the gradient computation still fails at nx = 1024, because the adjoint solve runs UMFPACK.
Details
  • Steady conduction in a slab: The patched function is fenics_adjoint.types.compat.linalg_solve. Evidence: https://github.com/pasteurlabs/mosaic/issues/180 (comment by the FEniCS developer who proposed the patch); merged in #182.
  • Upstream: Not filed: legacy dolfin-adjoint has had no release since 2019.1.0. Per #182, newer dolfin-adjoint passes linear_solver to the adjoint solve; per #180, DOLFINx-adjoint takes adjoint PETSc options.
  • Record id: mumps-adjoint-monkeypatch

Tapes the nodal misfit with a custom pyadjoint block L2 required

Puts the nodal identification error on the pyadjoint tape through a custom Block with hand-written adjoint and tangent-linear components, so pyadjoint differentiates it together with the solve.

  • Steady conduction in a slab (cannot express, incorrect gradient): The nodal sum cannot be written as a UFL integral, because legacy FEniCS has no vertex quadrature on hexahedra. An earlier integral approximation gave gradients that did not match the reported value.
Details
  • Steady conduction in a slab: The error is sum_v mask_v (T_v - T_target,v)^2. The approximation was an L2 integral rescaled by the node count. gradient/source_fd_check flagged FEniCS at 1753x the peer median, and conductivity_recovery_bfgs reached a final/initial misfit of 0.02779 against 0.00020 with the nodal adjoint. Evidence: #167; #184, notes; comment above NodalSquaredErrorBlock.
  • Record id: nodal-misfit-block

Setups and checks

Setup Effort Forward Gradient Optimization
Steady conduction in a slab L3 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/thermal-mesh/fenics-heat builds the image fenics_heat_thermal_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. ρ=1 → fully conducting, ρ=0 → insulating.
source Array[(None,), Float32] ✓ Per-element volumetric heat source (W/m³), shape (n_cells,). Active slice = source[:hex_mesh.n_faces]. Zero by default (topology-optimisation mode; no body heat source).
target_temperature Array[(None,), Float32] Per-node target temperature for the inverse (source-identification) objective, shape (n_nodes,). identification_error =
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe temperature (values shape (n_groups, 1), None → zero); neumann.mask/values prescribe heat flux (values shape (n_groups, 1)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
k_max float Maximum thermal conductivity (fully solid/conducting material).
p_exp float SIMP penalisation exponent p (k(ρ) = k_min + (k_max−k_min)·ρ^p).

Outputs

Field Type ∂ Description
thermal_compliance Array[(), Float32] ✓ Total thermal compliance C = ∮_Γ_N q_n · T dΓ (scalar). Equals the work done by the Neumann heat flux on the temperature field.
identification_error Array[(), Float32] ✓ Source-identification objective:

Firedrake

Firedrake P1 FEM heat-conduction solver with SIMP penalisation. Gradients (∂C/∂ρ, ∂C/∂source) via firedrake-adjoint, complementing the FEniCS reference.

Interventions and deviations

Abstraction level: framework.

Seeds the nodal misfit gradient by hand L2 unknown

Seeds the identification-error gradient with a hand-built cofunction at the nodes that have a target value, because the error is a nodal sum rather than an integral.

  • Steady conduction in a slab (incorrect gradient): An earlier integral approximation gave gradients that did not match the reported value. It is not known whether the hand-built seed is needed.
Details
  • Steady conduction in a slab: The cofunction is dI/dT = 2 (T - T_target) and seeds the derivative of the temperature functional. The approximation was an L2 integral rescaled by the node count. gradient/source_fd_check went from 4.58e-02 to 3.52e-05 with the nodal seed, and the rho gradient was 9% off central differences, with a conductivity_recovery_bfgs final/initial ratio of 0.00313 against 0.00020. Whether a vertex quadrature rule would let the nodal sum be written in UFL, which Firedrake’s custom quadrature may allow, was not checked. Evidence: #162; #167, summary; #184; current form from #229.
  • Record id: nodal-misfit-seed

Setups and checks

Setup Effort Forward Gradient Optimization
Steady conduction in a slab L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/thermal-mesh/firedrake-heat builds the image firedrake_heat_thermal_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. ρ=1 → fully conducting, ρ=0 → insulating.
source Array[(None,), Float32] ✓ Per-element volumetric heat source (W/m³), shape (n_cells,). Active slice = source[:hex_mesh.n_faces]. Zero by default (topology-optimisation mode; no body heat source).
target_temperature Array[(None,), Float32] Per-node target temperature for the inverse (source-identification) objective, shape (n_nodes,). identification_error =
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe temperature (values shape (n_groups, 1), None → zero); neumann.mask/values prescribe heat flux (values shape (n_groups, 1)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
k_max float Maximum thermal conductivity (fully solid/conducting material).
p_exp float SIMP penalisation exponent p (k(ρ) = k_min + (k_max−k_min)·ρ^p).

Outputs

Field Type ∂ Description
thermal_compliance Array[(), Float32] ✓ Total thermal compliance C = ∮_Γ_N q_n · T dΓ (scalar). Equals the work done by the Neumann heat flux on the temperature field.
identification_error Array[(), Float32] ✓ Source-identification objective:

JAX-FEM

JAX-FEM HEX8 steady-state heat-conduction solver with SIMP conductivity and optional volumetric source. Gradients ∂C/∂ρ and ∂C/∂source via JAX autodiff through the linear FE solve (hybrid implicit-function-theorem adjoint)

Interventions and deviations

Abstraction level: framework.

Compiles JAX-FEM’s BiCGSTAB solve once per matrix size L2 choice tuned

Passes JAX-FEM’s own Jacobi-preconditioned BiCGSTAB, compiled once per matrix size, to ad_wrapper through the custom_solver option for the forward and adjoint solves.

  • Steady conduction in a slab (too slow): The default jax_solver recompiles on every call, and the umfpack_solver option used before is about 12 times slower at N = 1024 (60.8 s against 5.17 s).
Details
  • Steady conduction in a slab: The custom solver uses the default jax_solver’s algorithm and tolerances. The default jax_solver runs bicgstab eagerly, so it compiles 2 XLA programs per forward call and 4 per VJP, a fixed overhead on every call. The umfpack_solver option gives identical outputs but is much slower on large meshes. At N = 1024, spatial_cost took 60.8 s with umfpack_solver and 5.17 s with this solver, vjp_cost/by_N 120 s and 10.8 s. Evidence: #286, before/after table; comment above _bicgstab.
  • Record id: jitted-bicgstab-solver

Setups and checks

Setup Effort Forward Gradient Optimization
Steady conduction in a slab L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/thermal-mesh/jax-fem builds the image jax_fem_thermal_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. ρ=1 → fully conducting, ρ=0 → insulating.
source Array[(None,), Float32] ✓ Per-element volumetric heat source (W/m³), shape (n_cells,). Active slice = source[:hex_mesh.n_faces]. Zero by default (topology-optimisation mode; no body heat source).
target_temperature Array[(None,), Float32] Per-node target temperature for the inverse (source-identification) objective, shape (n_nodes,). identification_error =
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe temperature (values shape (n_groups, 1), None → zero); neumann.mask/values prescribe heat flux (values shape (n_groups, 1)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.

Outputs

Field Type ∂ Description
thermal_compliance Array[(), Float32] ✓ Total thermal compliance C = ∮_Γ_N q_n · T dΓ (scalar). Equals the work done by the Neumann heat flux on the temperature field.
identification_error Array[(), Float32] ✓ Source-identification objective:

torch-fem

torch-fem SolidHeat HEX8 heat-conduction solver with IsotropicConductivity3D SIMP material — the first PyTorch backend in thermal-mesh. Gradients via PyTorch autograd through the FE solve; GPU-native.

Interventions and deviations

Abstraction level: framework.

Runs the solver on the GPU L0 choice

With a GPU present, runs torch-fem on it instead of torch’s default CPU device.

  • Steady conduction in a slab: No comparison with the CPU exists.
Details
  • Steady conduction in a slab: The tesseract has run on the GPU since its first version.
  • Record id: gpu-device

Sets torch’s default device and dtype globally L0 required

Sets torch’s default device and dtype (float64) process-wide, so the tensors torch-fem allocates internally land on the GPU with the mesh, material and load tensors.

  • Steady conduction in a slab (crash): torch-fem allocates auxiliary tensors without device=, so they land on the CPU and collide with the CUDA tensors.
Details
  • Steady conduction in a slab: No error log is recorded. Evidence: Code comment; torch-fem’s benchmarks/utils.py at v0.12.1 and its current installation docs set the default device the same way.
  • Record id: global-default-device

Solves with Jacobi-preconditioned CG on the GPU L0 choice

On the GPU, passes method=“cg” and preconditioner=“jacobi” to model.solve for the forward and adjoint solves. On the CPU torch-fem chooses.

  • Steady conduction in a slab (too slow): torch-fem 0.12.1 solves systems below 10,000 DOFs directly with SciPy on the CPU, even when the matrix is on the GPU.
Details
  • Steady conduction in a slab: Larger systems already get CG with Jacobi without AmgX, so the setting keeps the smaller meshes on the device. There is no isolated measurement. Evidence: #187; resolve_method and resolve_library in torch-fem v0.12.1 src/torchfem/sparse.py.
  • Record id: gpu-cg-jacobi

Loosens the CG stopping tolerance to 1e-6 L0 choice tuned

The GPU CG stops at a relative residual of 1e-6 (stol) instead of torch-fem’s 1e-10.

  • Steady conduction in a slab: No measurable gain here: cost moved by -16% to +11% against 1e-10, within the run-to-run spread on a shared GPU.
Details
  • Steady conduction in a slab: Every status is unchanged from 1e-12 to 1e-6. Forward metrics are bit-identical. Evidence: #301, notes.
  • Record id: cg-tolerance

Makes the CUDA assembly deterministic L0 choice

Enables torch.use_deterministic_algorithms(True, warn_only=True), so torch-fem’s CUDA scatter-adds in assembly sum in a fixed order.

  • Steady conduction in a slab (non reproducible): Without it, identical GPU runs differed: the conductivity_recovery_bfgs final loss varied between 241 and 5.7e3.
Details
  • Steady conduction in a slab: The spread depended on whether the run left a plateau before the iteration cap. With the setting, reruns are identical (#200, and again at every CG tolerance in #301). It cost about 5 to 15% in #200, on an RTX 2000 Ada. Evidence: #200, notes; #301, notes.
  • Record id: deterministic-kernels

Setups and checks

Setup Effort Forward Gradient Optimization
Steady conduction in a slab L0 Pass Pass Pass

Use it in your code

tesseract build mosaic/tesseracts/thermal-mesh/torch-fem-thermal builds the image torch_fem_thermal_mesh. To load it and call it from JAX, follow Use a solver in your code.

Fields marked ∂ are differentiable.

Inputs

Field Type ∂ Description
rho Array[(None,), Float32] ✓ Per-cell density field, shape (n_max_cells,). Active slice = rho[:hex_mesh.n_faces]. ρ=1 → fully conducting, ρ=0 → insulating.
source Array[(None,), Float32] ✓ Per-element volumetric heat source (W/m³), shape (n_cells,). Active slice = source[:hex_mesh.n_faces]. Zero by default (topology-optimisation mode; no body heat source).
target_temperature Array[(None,), Float32] Per-node target temperature for the inverse (source-identification) objective, shape (n_nodes,). identification_error =
boundary_conditions MeshBC Mesh boundary conditions. dirichlet.mask/values prescribe temperature (values shape (n_groups, 1), None → zero); neumann.mask/values prescribe heat flux (values shape (n_groups, 1)).
hex_mesh HexMesh Hexahedral mesh (8-node hex elements). n_points / n_faces give the active slice sizes.
k_max float Maximum thermal conductivity (fully solid material).
p_exp float SIMP penalisation exponent p (k(ρ) = k_min + (k_max−k_min)·ρ^p).

Outputs

Field Type ∂ Description
thermal_compliance Array[(), Float32] ✓ Total thermal compliance C = ∮_Γ_N q_n · T dΓ (scalar). Equals the work done by the Neumann heat flux on the temperature field.
identification_error Array[(), Float32] ✓ Source-identification objective: