Analysis and solving

DOF partitioning, the analysis cache, assembly, the in-place solve, and load cases/combinations/envelopes.

Asap.DofPartitionType
DofPartition

The 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 unknowns
  • fixed::Vector{Int}: global indices of support reactions
  • inactive::Vector{Int}: excluded slots
  • global_to_free::Vector{Int}: inverse map — 0 unless free, else the position in free (i.e. the row/column in the reduced system)
source
Asap.AnalysisCacheType
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 classification
  • groups::Vector{ElementGroup}: type-grouped elements with frozen scatter maps
  • K::SparseMatrixCSC{T,Int}: the free×free stiffness matrix with a FROZEN sparsity pattern — numeric assembly only rewrites nonzeros(K); no full-space matrix is ever formed
  • spring_entries::Vector{Tuple{Int,Int,Int}}: (nzval position, spring index in model.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 next solve! (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 vectors
  • q_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 recovery
  • factorization: cached factorization of K (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!).

source
Asap.assemble_K!Method
assemble_K!(cache::AnalysisCache, model) -> cache.K

Numerically (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.

source
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.

source
Asap.CachedSolverType
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.

source
Asap.process!Method
process!(model) -> model

Build 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.

source
CommonSolve.solve!Method
solve!(model; reprocess = false, solver = nothing) -> model

Run 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.

source
Asap.CaseResultsType
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 order
  • results::Vector{LinearResults{T}}: per-case results
  • F::Matrix{T}: per-case full-space load vectors P − Pf (columns) — needed to compute combined compliance
source
Asap.EnvelopeType
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 × n matrices of minima/maxima, rows ordered (N, Vy, Mz, Vz, My, Mx)
  • combos::Vector{Symbol}: names of the enveloped combinations
source
Asap.LoadCombinationType
LoadCombination{T}

A named, factored combination of load cases: Σ factor · case.

Fields

  • name::Symbol
  • factors::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])
source
Asap.combineMethod
combine(cr::CaseResults, combo::LoadCombination) -> LinearResults

Results 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.

source
Asap.envelopeMethod
envelope(model, el, cr::CaseResults, combos; resolution = 20) -> Envelope

Envelope 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.

source
Asap.load_casesMethod
load_cases(model) -> Vector{Symbol}

The distinct case tags present among the model's loads, in first-appearance order.

source
Asap.solve_cases!Method
solve_cases!(model; cases = load_cases(model)) -> CaseResults

Solve 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.

source

Internals

Asap.FactorizationCacheType
FactorizationCache

Holder 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.

source