Understand the solve pipeline#
The solve pipeline is the internal call chain executed each time
simu.Solve() is called. Every _Simu in the
EasyFEA.Simulations namespace runs the same pipeline, which lives in
EasyFEA/Simulations/_simu.py. This guide traces it from the moment you call
Solve() to the moment the solution is stored, aimed at advanced or curious
users who want to understand the internals without reading the full source.
Note
The internals described here live in EasyFEA/Simulations/_simu.py,
EasyFEA/Simulations/Solvers.py, and EasyFEA/FEM/_forms.py. The
single-underscore methods (e.g. _Solver_Apply_Dirichlet) are advanced API;
the double-underscore ones are private and should never be called directly.
Overview#
Every call to simu.Solve() performs the same three high-level operations:
Build — assemble the global sparse matrices \((K, C, M, F)\) from element integrals (skipped if nothing changed since the last solve).
Apply BCs — add Neumann contributions to the right-hand side, then enforce Dirichlet constraints to form the system \(A \, x = b\).
Solve — pass \(A \, x = b\) to the linear algebra backend (scipy, PETSc, pypardiso) and store the solution.
simu.Solve()
│
├── 1. BUILD ──────────────────────────────────────────────────────────
│ Get_K_C_M_F() (cached — rebuilds only if needUpdate)
│ └─ Assembly()
│ └─ Construct_local_matrix_system() ← your code
│ element integrals → K_e, C_e, M_e, F_e
│ COO scatter → global sparse K, C, M, F
│
├── 2. APPLY BCs ──────────────────────────────────────────────────────
│ _Solver_Apply_Neumann() add flux/load contributions to F
│ _Solver_Apply_Dirichlet() form A from K/C/M, enforce prescribed DOFs
│
└── 3. SOLVE ──────────────────────────────────────────────────────────
_Solve_Axb() scipy / PETSc / pypardiso
_Solver_Update_solutions() recover u, v, a from time scheme
_Set_solutions() store solution on the simulation object
For non-linear problems (simu.isNonLinear = True), steps 1–3 are wrapped
in a Newton–Raphson loop that repeats until the residual converges — each
iteration rebuilds the tangent stiffness and solves for the increment \(\Delta u\).
Step 1 — Build: matrix assembly#
Caching (Get_K_C_M_F)#
Get_K_C_M_F(problemType) returns the global matrices \((K, C, M, F)\).
It maintains a dirty flag (needUpdate) and only calls Assembly()
when the flag is set. The flag is raised automatically when:
the mesh geometry changes,
model material properties change,
a non-linear iteration updates the solution state.
When nothing has changed, the cached matrices are returned immediately — so repeated solves with the same system cost almost nothing extra.
# force a manual rebuild on the next Solve() (rarely needed)
simu.Need_Update()
Global assembly (Assembly)#
Assembly(problemType) turns element arrays into global sparse matrices:
Calls
Construct_local_matrix_system()to get element arrays(K_e, C_e, M_e, F_e), each of shape(Ne, nPe·dof, nPe·dof).Retrieves the global DOF index maps (
rows_e,columns_e) from the element group.Scatters all element contributions into global sparse CSR matrices.
Local integration (Construct_local_matrix_system)#
This is the method you implement when subclassing
_Simu. It returns the element-level arrays
for one physics. When using WeakForms,
EasyFEA generates it automatically from @BiLinearForm / @LinearForm
decorators — but you can always implement it directly for full control.
Each BiLinearForm evaluates the weak-form integrand at all Gauss points
simultaneously (vectorized over all Ne elements) — no Python loops.
Step 2 — Apply boundary conditions#
Neumann (natural) BCs#
_Solver_Apply_Neumann(problemType) adds all Neumann loads (surface tractions,
line loads, point forces) to the right-hand side vector \(F\). These were
registered with add_neumann, add_surfLoad, etc.
Dirichlet (essential) BCs#
_Solver_Apply_Dirichlet(problemType, b, resolution) constructs the effective
system matrix \(A\) from the assembled \(K\), \(C\), \(M\) weighted by the
time-integration scheme, then eliminates prescribed DOFs:
Problem type |
System matrix \(A\) |
|---|---|
Elliptic (static) |
\(A = K\) |
Parabolic (heat, diffusion) |
\(A = K + \frac{1}{\alpha \Delta t} C\) |
Newmark / Midpoint / HHT |
\(A = c_K K + c_C C + c_M M\) |
For non-linear problems, \(A\) is built from the tangent stiffness \(K_t\), and prescribed values are converted to incremental form \(\Delta u_D = u_D - u_D^{\text{current}}\) so that the Newton correction is consistent.
Step 3 — Solve#
Linear solve (_Solve_Axb)#
_Solve_Axb selects a resolution strategy based on the BCs present, then
dispatches to the active backend:
Strategy |
When |
Approach |
|---|---|---|
|
Standard Dirichlet |
Partition into known/unknown DOFs; solve reduced system |
|
Lagrange multiplier BCs |
Augment system with Lagrange multipliers |
Backend |
When active |
|---|---|
|
Serial, no MPI |
|
Serial, Intel MKL available |
|
MPI ( |
In parallel, PETSc solves the distributed system and EasyFEA performs an
Allreduce so that every rank holds the same complete DOF solution — no
manual gather needed inside the solve loop.
Recover and store (_Solver_Update_solutions + _Set_solutions)#
_Solver_Update_solutions applies the active time-integration scheme to
recover all solution fields from the solved \(u\):
Scheme |
Recovers |
|---|---|
Elliptic |
\(u\) only |
Parabolic |
\(u\), \(\dot{u}\) (via backward Euler / Crank–Nicolson) |
Newmark / HHT / Midpoint |
\(u\), \(\dot{u}\), \(\ddot{u}\) |
_Set_solutions then stores all fields on the simulation object
(simu.u, simu.v, simu.a).
Non-linear problems: Newton–Raphson loop#
When isNonLinear = True, steps 1–3 are wrapped in a loop. The loop solves
for an increment \(\Delta u\) at each iteration and accumulates it:
u ← Get_x0() start from the current solution state
REPEAT:
Need_Update() mark K as dirty so it is rebuilt with current u
Δu = Solve_simu(...) solve Kt Δu = −R(u)
u += Δu accumulate the Newton correction
check ‖R‖ < tolConv convergence on residual norm
OR ‖Δu‖ < 1e-11 convergence on increment norm
UNTIL converged OR maxIter reached
Construct_local_matrix_system must return:
K_e— the tangent stiffness \(K_t = \partial R / \partial u\)F_e— the residual \(R(u)\)
Iteration management (Save_Iter / Set_Iter)#
After each time step or load increment, call
Save_Iter() to snapshot the current
solution:
simu.Save_Iter() # stores u (and v, a if present) for this step
Derived quantities (stress, strain, …) are not stored — they are
recomputed on demand by Result().
To restore a previous state (e.g. for post-processing or animation):
simu.Set_Iter(i) # restores u, v, a to the values saved at step i
Important
If the simulation uses different meshes across iterations (adaptive
remeshing, crack propagation, phase-field with refinement), Set_Iter
also restores the mesh that was active at step i. No manual mesh switching
is needed when replaying history or generating animations.
EasyFEA beyond forward solves#
The FEM infrastructure — mesh.Get_* functions, FeArray arithmetic,
Gauss-point integration — is not restricted to _Simu subclasses. You can
use it directly to evaluate arbitrary integrals or construct custom operators
over a mesh, without ever calling Solve().
DIC (Digital Image Correlation) is the
canonical example: it is a full analysis class built on the same mesh and
integration machinery, but it never solves a linear system in the traditional
sense. Instead it assembles correlation operators directly from mesh.Get_*
functions and minimises a correlation functional.
See Digital Image Correlation (DIC) analyses for worked examples.
This means EasyFEA can serve as a general-purpose FEM toolkit for any computation that benefits from structured Gauss-point integration over a mesh, not only for forward PDE solves.