Shape (mesh-coordinate) derivatives

Hermit differentiates the shell solve and every output functional with respect to the mesh node coordinates. This is the sensitivity a shape optimizer, or the chain rule back to a geometry parameterisation, consumes.

Using it

Build a geometry from a csdl.Variable as either:

  • node_disp= — a displacement field added to the reference coordinates (nodes = reference + node_disp), or

  • nodes= — absolute node coordinates.

nd = csdl.Variable(value=np.zeros((domain.n_nodes, 3)), name="node_disp")
state = hm.solve(domain, material, loads, bcs,
                 geometry=hm.geometry(domain, node_disp=nd))
compliance = hm.compliance(state)

dC_dX = sim.compute_totals([compliance], [nd])[compliance, nd].reshape(domain.n_nodes, 3)

dC_dX[k] is \(\partial(\text{compliance})/\partial\mathbf{x}_k\) at mesh node \(k\), in your node ordering.

Without a live node_disp / nodes variable the geometry is treated as constant and no mesh-derivative forms are built — there is no cost to leaving it out.

How it works

The residual and output forms are differentiated with respect to ufl.SpatialCoordinate (a UFL CoordinateDerivative). The resulting sensitivity lives on the mesh coordinate space; Hermit scatters it back to the user’s node ordering via mesh.geometry.input_global_indices.

mesh.geometry.x is used as a scratch buffer during assembly and restored afterwards, so a solve never leaves the mesh moved for a later ShellDomain solve.

Requirements and caveats

  • Serial mesh whose input_global_indices form a node permutation (true for a normally-read serial mesh). This is not specific to shape derivatives — ShellDomain enforces it for every use, so an MPI-partitioned mesh raises before any solve.

  • UFL patch. UFL’s CoordinateDerivative handler crashes on the shell bending term (grad(cross(CellNormal, theta))) — in both UFL 2024.2 (DOLFINx 0.9) and 2026.1 (DOLFINx 0.11). Hermit applies a small runtime monkeypatch (hermit._ufl_compat) on import hermit, adapting to whichever UFL ruleset API is present; the installed UFL files are untouched.

  • Non-smooth stabilization scale. UFL drops the (non-differentiable) subgradient of CellDiameter, which appears only in the drilling / penalty stabilization scale. It is a stabilization term rather than a physical one, so the effect on outputs is small, but the size has not been quantified against a benchmark.

  • Penalty conditioning. With a very large geometry perturbation the penalty-BC system becomes ill-conditioned and finite-difference checks stop converging — a conditioning limit, not a bug. Use the strong-BC path or a smaller step for verification.

The mesh-coordinate adjoints are finite-difference validated in tests/test_mesh_coord_deriv.py, which measures a few parts in \(10^{5}\) relative error and gates at \(3\times10^{-3}\); the ex_shape_derivative_fd.py benchmark gates at \(5\times10^{-3}\). Those gates are set by finite-difference truncation and cancellation, not by the adjoint.