Solvers#
femorph-solver separates the assembly step from the linear and eigen solves, and every solve runs against a registry of pluggable backends. This page documents what’s registered, how to select a backend, and the measured trade-offs between them.
Pipeline#
A full static or modal call goes through three stages:
Assembly —
femorph_solver.Modelbuilds the globalK(andMfor modal / transient) directly in CSR format viafemorph_solver._core.build_csr_pattern+femorph_solver._core.scatter_into_csr. Both are nanobind entry points that run on batched per-element matrices returned by the element kernels (solid18X_ke_batch,solid18X_me_*_batch, …). The symbolic pattern is cached on the model, soMafterKskips the symbolic pass entirely.Linear solve —
femorph_solver.solvers.static.solve_static()partitionsK u = Finto free/fixed DOF blocks and dispatchesu_fto a registered linear backend.Generalized eigensolve —
femorph_solver.solvers.modal.solve_modal()dispatches to a registered eigen backend (default: ARPACK in shift-invert mode with σ = 0), which itself calls a registered linear backend for its inner(K - σM)factorisation.
The registries are exposed as two module-level lookup helpers plus a default-resolver:
femorph_solver.solvers.linear.list_linear_solvers()— rows of{name, available, kind, spd_only, install_hint}femorph_solver.solvers.eigen.list_eigen_solvers()— same shapefemorph_solver.solvers.linear.select_default_linear_solver()— resolves"auto"to the fastest available SPD backend.
Linear backends#
auto is the default in every place that takes a linear_solver
keyword (Model.solve, Model.modal_solve,
solve_static(),
solve_modal(), and every eigen backend’s
shift-invert). Resolution is size-aware: above 50 000 free DOFs the
preference is pardiso → cholmod → umfpack → superlu (Pardiso’s
threaded factor wins by 3–5×); below that threshold Pardiso is demoted
behind CHOLMOD because its ~200 ms setup cost erases the factor-time
lead on small systems. Users who install an optional backend later
pick it up with no code change.
If a solve crosses 50 000 DOFs on a machine where Pardiso would have
been chosen but pypardiso is not installed, femorph-solver emits a one-shot
UserWarning pointing at pip install femorph_solver[pardiso]. The
warning fires at most once per process, so a modal sweep or sensitivity
study with many solves won’t flood the logs.
Name |
Kind |
SPD |
Notes |
Install |
|---|---|---|---|---|
|
direct |
SciPy’s built-in SuperLU. Always available. Baseline accuracy and memory behaviour; good fallback. |
none |
|
|
direct |
SuiteSparse UMFPACK. General (non-SPD-restricted) LU with AMD ordering. Slightly faster than SuperLU on unsymmetric systems. |
|
|
|
direct |
✓ |
SuiteSparse supernodal Cholesky. Dramatically lower fill-in than LU on SPD stiffness matrices — typically ~2× faster and ~2× less peak RSS than SuperLU on femorph-solver’s modal pipeline. |
|
|
direct |
Intel MKL PARDISO via pypardiso. Fastest when installed on
Intel hardware; multithreaded out of the box. First in the
|
|
|
|
iterative |
✓ |
SciPy conjugate gradient with Jacobi preconditioner. Not competitive on stiffness matrices without a stronger preconditioner — avoid for ARPACK shift-invert on real models. |
none |
|
iterative |
SciPy GMRES with Jacobi preconditioner. Same caveat as CG. |
none |
|
|
iterative |
✓ |
Smoothed-aggregation algebraic multigrid. Useful research target; needs per-problem tolerance tuning to match direct solver accuracy. |
|
Select explicitly by name:
import femorph_solver
m = femorph_solver.Model()
# ... build mesh + BCs ...
res = m.solve(linear_solver="cholmod") # explicit
modal = m.modal_solve(n_modes=10,
linear_solver="auto") # preference chain
Eigen backends#
Modal solves live in femorph_solver.solvers.modal. Below
DENSE_CUTOFF (500 free DOFs) femorph-solver always uses dense LAPACK
(scipy.linalg.eigh()); above it, the eigen_solver kwarg
selects one of the registered backends.
Name |
Kind |
Notes |
|---|---|---|
|
sparse Lanczos |
Default. Uses |
|
sparse iterative |
|
|
Davidson |
Optional — requires |
|
LAPACK dense |
Small problems only. Materialises |
Measured trade-offs#
Numbers below are from the solver-framework bench
(perf/bench_solvers.py) on flat SOLID185 plates at two sizes,
OMP_NUM_THREADS=4 (matching MAPDL’s default thread cap). See
Performance for the full progression and the larger pipeline
sweep.
The relative ordering is size-dependent: SuiteSparse CHOLMOD wins on small problems (its setup is close to free), and Intel MKL PARDISO wins on medium/large problems (its parallel factorisation recoups its heavier setup). SuperLU is always a reasonable zero-install baseline.
Linear backends inside ARPACK shift-invert (σ = 0, 10 lowest modes, tol = 1e-10):
backend |
kind |
SPD |
60×60×2 plate |
100×100×4 plate |
notes |
|
|---|---|---|---|---|---|---|
32 940 DOFs |
151 500 DOFs |
Δf₁ (Hz) |
||||
superlu |
direct |
1.10 s |
24.7 s |
1.4 × 10⁻⁷ |
SciPy default — always available |
|
cholmod |
direct |
✓ |
1.03 s |
23.1 s |
1.2 × 10⁻¹⁴ |
fastest at ~30k DOFs; falls behind PARDISO past ~100k |
pardiso |
direct |
1.63 s |
7.3 s |
3.5 × 10⁻⁷ |
3.4× faster than SuperLU/CHOLMOD at 150k DOFs |
|
cg |
iterative |
✓ |
>120 s |
>120 s |
— |
Jacobi insufficient for stiffness matrices |
gmres |
iterative |
>120 s |
>120 s |
— |
same caveat as CG |
|
pyamg |
iterative |
✓ |
65.9 s |
— |
8.9 × 10¹ |
wrong eigenvalues at default tol; tuning required |
Standalone eigen backends (without wrapping ARPACK):
backend |
kind |
60×60×2 wall |
100×100×4 wall |
Δf₁ (Hz) |
notes |
|---|---|---|---|---|---|
arpack |
sparse Lanczos |
1.14 s |
23.9 s |
7 × 10⁻¹⁴ |
default; SuperLU shift-invert inside |
lobpcg |
sparse iterative |
35.5 s |
17.1 s |
3.4 × 10⁻⁷ |
ILU preconditioner; erratic at small N |
primme |
Davidson |
— |
— |
— |
not installed in the bench |
dense |
dense |
— |
— |
— |
disabled for N > 5 000 |
The PARDISO crossover above ~100k DOFs is the single biggest reason
to install femorph_solver[pardiso] on MKL hosts — the per-call setup cost
amortises quickly once the direct factorisation has real parallel
work to do. Below ~50k DOFs its overhead is net-negative, which is
why the default auto chain prefers it only when it is
installed, and CHOLMOD / SuperLU stay close behind as fallbacks.
Recommendations#
Default (structural modal / static) — leave
linear_solverat"auto"and installfemorph_solver[cholmod]. At <50k DOFs CHOLMOD matches or beats the alternatives; at >100k DOFs it still beats SuperLU roughly 1:1 in wall time while using less memory. Falls back to SuperLU automatically if CHOLMOD is removed.Intel MKL hosts, ≥100k DOFs — add
femorph_solver[pardiso]. PARDISO rises to the top of theautochain and is typically 3×–5× faster than SuperLU / CHOLMOD on the shift-invert inner solve.Unsymmetric / non-SPD systems — pass
linear_solver="auto"withspd=False(or pickumfpack/superluby name).Iterative solvers — reserve for research. The registered Krylov + AMG backends exist for benchmarking, not as drop-in replacements for CHOLMOD / PARDISO on stiffness matrices.
Install extras#
extra |
pulls in |
enables |
|---|---|---|
(none) |
SciPy |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
all of the above |
every registered backend |
Missing backends raise femorph_solver.solvers.linear.SolverUnavailableError
with the matching install hint, so users see how to enable them.