Differentiable analysis

The entire pipeline has a pure functional path: extract a ModelState (positions and sections as plain data), evaluate, differentiate. Loading Zygote (or anything ChainRules-aware) activates Asap's rule extension automatically; a handful of small rules cover the sparse construction and the linear solve, and everything else differentiates natively.

The example differentiates the compliance of a small portal frame model with respect to every node coordinate:

using Zygote

solve!(model)                               # builds the analysis cache
state = extract_state(model)

# gradient of compliance w.r.t. every node coordinate — a 3 × n sensitivity field:
g = Zygote.gradient(state.X) do X
    compliance(model, ModelState{Float64}(X, state.sections))
end[1]
3×4 Matrix{Float64}:
 -0.00149367   0.00124859   0.00259249  -0.0023474
 -1.57914e-19  2.0817e-19   1.9245e-19  -2.42707e-19
 -0.000569496  0.000580041  0.00236257  -0.00237311

Gradients flow with respect to node positions, section properties, and semi-rigid connection stiffnesses. For design-variable bookkeeping (areas, coupled symmetric geometry, bounds) and optimization-ready objectives, use AsapOptim — a thin layer over this path, verified against the original published implementation to 13 digits (see its docs/AD_VERIFICATION_AND_BENCHMARKS.md).

Forward-mode AD (ForwardDiff, Enzyme forward) and Mooncake are supported through the same path via their own package extensions — load the AD package and differentiate; no further setup.