Analysis and solving
DOF partitioning, the analysis cache, assembly, the in-place solve, and load cases/combinations/envelopes.
Asap.DofPartition — Type
DofPartitionThe classification of every global degree of freedom into exactly one of three states — the backbone of the analysis layer:
- free: active (some element or spring couples stiffness to it) and not fixed by a support → an unknown of the solve
- fixed: fixed by a nodal support (
fixity = false) → a reaction slot - inactive: free of supports but touched by no stiffness → excluded from the system entirely. This is what removes truss-node rotations and fully released end rotations structurally: no zero-stiffness rows, no singular modes, no regularization.
Global DOF numbering: node i owns slots 6(i−1)+1 … 6i ordered (Tx, Ty, Tz, Rx, Ry, Rz); internal (non-nodal) element DOFs follow after all nodal slots.
Fields
n_global::Int: total DOF slots (nodal + internal)free::Vector{Int}: global indices of solve unknownsfixed::Vector{Int}: global indices of support reactionsinactive::Vector{Int}: excluded slotsglobal_to_free::Vector{Int}: inverse map —0unless free, else the position infree(i.e. the row/column in the reduced system)
Asap.AnalysisCache — Type
AnalysisCache{T}The analysis structure derived from a Model by process! — everything the solver needs that depends only on topology, built once and reused across solves:
partition::DofPartition: free/fixed/inactive classificationgroups::Vector{ElementGroup}: type-grouped elements with frozen scatter mapsK::SparseMatrixCSC{T,Int}: the free×free stiffness matrix with a FROZEN sparsity pattern — numeric assembly only rewritesnonzeros(K); no full-space matrix is ever formedspring_entries::Vector{Tuple{Int,Int,Int}}: (nzval position, spring index inmodel.springs, component 1:6) triples for nodal springs on free DOFs — VALUES are read fresh from the model at every assembly, so replacing a spring with a stiffer/softer one is picked up by the nextsolve!(adding stiffness on a previously-zero component changes the pattern and requires re-processing)P,Pf::Vector{T}: full-space nodal-load and fixed-end-force vectorsq_local::Vector{Vector{Vector{T}}}: per element, per SEGMENT, the accumulated LOCAL condensed fixed-end force 12-vector (primitive elements have one segment) — needed for element force recoveryfactorization: cached factorization ofK(set on first solve; numeric refactorization reuses the symbolic analysis since the pattern is frozen)
Geometry/section/load values may change freely between solves; adding or removing nodes, elements, springs, or changing end conditions in a way that alters DOF activity requires re-processing (process!).
Asap.assemble_K! — Method
assemble_K!(cache::AnalysisCache, model) -> cache.KNumerically (re)assemble the free×free global stiffness matrix in place. Element kernels read current node positions and section properties, so geometry/section changes since the last call are picked up automatically — only topology changes require re-processing.
Asap.assemble_loads! — Method
assemble_loads!(cache::AnalysisCache, model) -> (cache.P, cache.Pf)Build the full-space nodal load vector P and fixed-end force vector Pf from the model's loads.
Node loads accumulate directly into P (erroring usefully if a component targets an inactive DOF — e.g. a moment on a node connected only to truss elements — instead of surfacing later as a bare SingularException).
Element loads run the generic lowering: clamped local FEFs from the load's fixed_end_forces kernel → end-condition condensation (condense_fef) → accumulate locally in cache.q_local (for force recovery) and globally (blockwise Λᵀ rotation) into Pf.
Asap.CachedSolver — Type
CachedSolver(solver = nothing)A solver-seam wrapper that reuses ONE factorization for every solve with the same stiffness values: repeated _factorize calls compare K.nzval to the last factorized values — identical values return the stored factorization outright, changed values (same frozen pattern) trigger a numeric-only refactorization.
This is what makes derivative evaluation cheap in an optimization loop. At a design iterate x, the objective gradient (reverse: forward solve + adjoint backsolve), the constraint Jacobian (forward: one tangent system per ForwardDiff chunk), and any plain re-evaluation all assemble the SAME K(x) — with a CachedSolver they share one factorization instead of paying one each (a chunked 512-variable ForwardDiff Jacobian alone costs ~43 otherwise).
p = OptParams(model, vars; solver = Asap.CachedSolver())CONTRACT: derivative calls must be atomic — the pullback/tangent of one evaluation must run before the next evaluation at a DIFFERENT design begins, because refactorization mutates the stored factorization in place (a reverse-mode pullback holds a reference to it). Every standard driver (Zygote.gradient/withgradient/jacobian, ForwardDiff.gradient/ jacobian, DifferentiationInterface prepared operators, optimizer loops built on these) satisfies this; hand-held Zygote.pullback closures called after later evaluations do not. Use one CachedSolver per model/OptParams — the frozen sparsity pattern is assumed constant.
Asap.process! — Method
process!(model) -> modelBuild the model's analysis structure: assign node/element indices, classify DOFs (free / fixed / inactive), group elements by type, and freeze the free×free sparsity pattern with its scatter maps (see AnalysisCache).
Call once per topology. Geometry, section, spring-value, and load changes do NOT require re-processing — they are picked up by the next solve!. Changes that alter connectivity or DOF activity (adding/removing nodes/elements/springs, changing end conditions between released and engaged) do.
CommonSolve.solve! — Method
solve!(model; reprocess = false, solver = nothing) -> modelRun a linear static analysis: assemble the stiffness matrix and load vectors in place, factorize (Cholesky, with LDLᵀ fallback for indefinite spring cases), solve for the free displacements, and post-process element end forces and support reactions into a fresh LinearResults stored at model.results.
Repeated solves reuse the frozen sparsity pattern, all buffers, AND the factorization's symbolic analysis (numeric-only refactorization). Pass reprocess = true after topology changes.
solver selects the linear-solver backend. The default (nothing) is the built-in CHOLMOD path and needs no extra packages. With LinearSolve.jl loaded, any of its algorithms works — solve!(model; solver = KrylovJL_CG()) — and the choice is remembered on the model's cache for subsequent solves.
Asap.CaseResults — Type
CaseResults{T}Results of a multi-case solve (solve_cases!): one LinearResults per load case, all obtained from a single stiffness assembly and a single factorization. Combine into any LoadCombination's results with combine — pure superposition, no re-solve.
Fields
cases::Vector{Symbol}: the case tags, in solve orderresults::Vector{LinearResults{T}}: per-case resultsF::Matrix{T}: per-case full-space load vectorsP − Pf(columns) — needed to compute combined compliance
Asap.Envelope — Type
Envelope{T}Station-wise extrema of internal forces over a set of load combinations — the quantity a design check actually consumes.
Fields
x::Vector{T}: stations along the member [length]lo,hi:6 × nmatrices of minima/maxima, rows ordered (N, Vy, Mz, Vz, My, Mx)combos::Vector{Symbol}: names of the enveloped combinations
Asap.LoadCombination — Type
LoadCombination{T}A named, factored combination of load cases: Σ factor · case.
Fields
name::Symbolfactors::Vector{Pair{Symbol,T}}: e.g.[:dead => 1.2, :live => 1.6]
Examples
julia> strength = LoadCombination(:LRFD1, [:dead => 1.2, :live => 1.6])
julia> service = LoadCombination(:service, [:dead => 1.0, :live => 1.0])Asap.combine — Method
combine(cr::CaseResults, combo::LoadCombination) -> LinearResultsResults of a factored combination by superposition: displacements, reactions, and element end forces are linear in the loading, so the combined results are exact — no additional solve. (Compliance, being quadratic, is recomputed from the combined vectors.)
Cases named in the combination but absent from the solve contribute nothing; a warning-free contract — validate combination spelling upstream if needed.
Asap.envelope — Method
envelope(model, el, cr::CaseResults, combos; resolution = 20) -> EnvelopeEnvelope a member's internal forces over the given combinations: each combination's diagrams come from superposed case results (single factorization behind all of it), and the envelope is their station-wise extrema.
Asap.load_cases — Method
load_cases(model) -> Vector{Symbol}The distinct case tags present among the model's loads, in first-appearance order.
Asap.solve_cases! — Method
solve_cases!(model; cases = load_cases(model)) -> CaseResultsSolve every load case against ONE stiffness assembly and ONE factorization: the expensive work is done once, each case costs a load-vector assembly and a pair of triangular back-substitutions. Combinations then come free via combine.
Internals
Asap.FactorizationCache — Type
FactorizationCacheHolder for the factorization state behind the solver seam: which solver backend produced it (nothing = the built-in CHOLMOD path), the backend factorization object F, and backend-private meta (the built-in path stores whether it fell back to LDLᵀ). Opaque solver state — declared tangent-free for AD engines, so ANY backend's internals stay invisible to differentiation.