Architecture

Hermit separates reusable finite-element setup from differentiable problem data. ShellDomain owns the mesh, the mixed shell space and the ordering maps, and caches the compiled ShellPDE on first solve; Field is the common representation for material, load, geometry, and output fields.

The mesh must be serial: ShellDomain requires its vertex numbering to be a permutation of the mesh-file order, and raises on an MPI-partitioned mesh. This applies to every use, not only to shape derivatives.

domain = hm.ShellDomain(mesh, element="CG2CG1")
bcs = hm.clamp(domain, where=root)
mat = hm.isotropic(domain, E=E, nu=nu, thickness=t, density=rho)
state = hm.solve(domain, mat, hm.pressure(domain, pz), bcs)
compliance = hm.compliance(state)

The domain and boundary conditions are constructed once and reused across cases. solve returns a ShellState that retains its domain, geometry, material, loads, and boundary conditions, so each output requires only that state. The FEniCSx assembly and linear/adjoint solves remain behind custom operations; CSDL sees differentiable inputs and outputs.

Fields and ordering

Field.coeffs are in FE-dof order. hm.from_coeffs accepts that order directly, while hm.from_nodal and hm.from_cells accept mesh-file vertex and cell order respectively. Use domain.dof_coords(space) with hm.from_function or hm.from_coeffs when constructing coefficients on other spaces. Input fields can use independent spaces; no domain-wide material or load space exists.

hm.interpolate transfers between spaces on the same mesh by reference-element collocation. hm.project is the geometry-dependent L2 alternative.

Materials, loads, and boundary conditions

hm.isotropic, hm.laminate, and hm.composite construct a Material. Composite ABD matrices remain in laminate axes and an Orientation from hm.fiber_angle or hm.fiber_direction is consumed inside the shell form.

hm.pressure, hm.traction, hm.moment, hm.point_load, and hm.load_vector each return composable Loads; hm.edge_pressure, hm.edge_traction, and hm.edge_moment apply the same loads per unit length on exterior facets selected by a where= predicate. Every load term reaches the residual and the compliance form on its own space, so mixing spaces costs no interpolation.

Use hm.clamp, hm.pin, hm.symmetry, and hm.gauge for boundary conditions. Each takes value= for a non-zero prescribed displacement or rotation, and + merges them with later terms winning on overlapping entities. A prescribed value is interpolated once rather than carried as a solve input, so unlike a load it cannot be a csdl.Variable. See Background.

Outputs and frames

Scalar outputs are individual functions: hm.compliance, hm.mass, hm.center_of_gravity, hm.elastic_energy, stress aggregates, and composite hm.failure_index. hm.strain_fields(state, space=..., method=..., frame=...) returns membrane, bending, and shear Fields; hm.displacement_field and hm.rotation_field return state fields.

A DG strain field defaults to an element-local frame, while a CG field defaults to a global Cartesian frame. Use to_global() or to_frame(...) to change representation. Local-frame components require DG because neighbouring element frames need not agree; global-frame components can be continuous.

Geometry and surrogates

The reference geometry is used by default. Pass geometry=hm.geometry(domain, node_disp=shape) (or nodes=...) to make geometry a differentiable input. A surrogate can use the same arguments as hm.solve and return a ShellState; state-based output functions can then consume that result.