1-D Finite Elements: Stiffness Assembly
Build a complete finite element solver for an axial bar: linear shape functions, element stiffness matrix and load vector, global assembly, applying boundary conditions, solving, and post-processing stresses and reactions. Understand why linear elements give exact nodal values in 1-D, how convergence works, and how the same steps scale to 2-D/3-D FEM software.
Commercial FEM packages (ANSYS, Abaqus, COMSOL) hide millions of equations, but every one follows the same five steps: discretise, build element matrices, assemble, apply boundary conditions, solve and post-process. Doing it once by hand for a bar demystifies the whole field.
An axial bar is the simplest case: each 2-node element acts like a spring of stiffness EA/h, and assembling elements is like connecting springs in series.
An analogy
Assembling a stiffness matrix is like building with LEGO: each element brick has its own small matrix, and the global structure is formed by snapping bricks together at shared nodes — where their stiffnesses add.
The five FEM steps
Every FE analysis, from a bar to a jet engine, follows:
- 1. Discretise: split the domain into elements joined at nodes.
- 2. Element equations: kₑ and fₑ from the weak form with shape functions.
- 3. Assemble: add element contributions into global K and F by node numbers.
- 4. Boundary conditions: remove/fix constrained DOFs, add point loads.
- 5. Solve Ku = F, then compute strains, stresses and reactions.
Where kₑ comes from
Substitute u = N₁u₁ + N₂u₂ into the weak form: kᵢⱼ = ∫EA Nᵢ′Nⱼ′ dx. With N₁′ = −1/h and N₂′ = 1/h, this gives (EA/h)[[1, −1], [−1, 1]] — exactly a spring of stiffness EA/h. The load vector ∫qNᵢ dx splits a uniform load equally between the two nodes.
Properties of K
K is symmetric, banded (each node couples only to neighbours) and singular before supports are applied — the rigid-body motion has zero energy. Applying u(0) = 0 removes that mode and makes K positive definite, so efficient Cholesky or sparse solvers can be used.
Scaling up
In 2-D/3-D, elements are triangles, quads, tets or hexes; shape functions are defined on a reference element and mapped with the Jacobian (isoparametric formulation); integrals are done by Gauss quadrature; K has millions of rows but is extremely sparse. Mesh convergence studies — refining until results change by less than a few percent — are mandatory in professional practice.
Formulas
- Linear shape functions:
N₁ = 1 − x/h, N₂ = x/h; u(x) = N₁u₁ + N₂u₂ - Element stiffness:
kₑ = (EA/h) [[1, −1], [−1, 1]] - Consistent load (UDL q):
fₑ = (qh/2) [1, 1]ᵀ - Assembly:
K = Σₑ Lₑᵀ kₑ Lₑ (add into rows/cols of the element's nodes) - Solve (free DOFs):
K_ff u_f = F_f; reactions R = K_rf u_f − F_r - Element stress:
σₑ = E (u₂ − u₁)/h - Exact bar solution:
u(x) = [q(Lx − x²/2) + P x] / EA
Worked example: Assembling two elements
Given: Two equal elements, stiffness k each
Find: Global K.
- Element 1 → rows/cols 1,2; element 2 → rows/cols 2,3
- Overlap at node 2 adds
Answer: K = k [[1, −1, 0], [−1, 2, −1], [0, −1, 1]]
Worked example: Stepped bar
Given: E = 200 GPa; A₁ = 200 mm², A₂ = 100 mm², each 0.5 m; P = 20 kN at tip, fixed at root
Find: Displacements and stresses.
- k₁ = 200e9·200e−6/0.5 = 8×10⁷ N/m; k₂ = 4×10⁷ N/m
- u₂ = P/k₁ = 0.25 mm; u₃ = u₂ + P/k₂ = 0.75 mm
- σ₁ = 20 000/200 = 100 MPa; σ₂ = 200 MPa
Answer: u = [0, 0.25, 0.75] mm; σ = 100, 200 MPa
Worked example: Reaction
Given: Bar with UDL q = 5 kN/m over 2 m plus P = 10 kN
Find: Support reaction.
- R = −(qL + P) (equilibrium)
- FE recovers it from row 1: R = K₁ᵢuᵢ − F₁
Answer: R = −20 kN (acting opposite to the loads)
Complete 1-D bar FEM solver
import numpy as np
# Axial bar: -EA u'' = q on [0, L], u(0) = 0, EA u'(L) = P
EA, L, q, P = 2.0e7, 2.0, 5.0e3, 1.0e4 # N, m, N/m, N
for ne in (1, 2, 4):
n = ne + 1
h = L / ne
K = np.zeros((n, n)); F = np.zeros(n)
ke = EA / h * np.array([[1, -1], [-1, 1]]) # element stiffness
fe = q * h / 2 * np.array([1, 1]) # consistent load vector
for e in range(ne): # assembly
idx = [e, e + 1]
K[np.ix_(idx, idx)] += ke
F[idx] += fe
F[-1] += P # point load at the tip
u = np.zeros(n)
u[1:] = np.linalg.solve(K[1:, 1:], F[1:]) # apply u(0) = 0
print(f"{ne} element(s): tip u = {u[-1]*1e3:.4f} mm, mid u = {np.interp(L/2, np.linspace(0, L, n), u)*1e3:.4f} mm")
x = L / 2
print(f"exact: tip u = {(q*L**2/2 + P*L)/EA*1e3:.4f} mm, mid u = {((q*L*x - q*x**2/2) + P*x)/EA*1e3:.4f} mm")
# 1 element(s): tip u = 1.5000 mm, mid u = 0.7500 mm
# 2 element(s): tip u = 1.5000 mm, mid u = 0.8750 mm
# 4 element(s): tip u = 1.5000 mm, mid u = 0.8750 mm
# exact: tip u = 1.5000 mm, mid u = 0.8750 mmNodal displacements are exact for any mesh (a special 1-D property), but between nodes the linear interpolation is wrong until a node sits there — with one element the midpoint is 14% low. Stresses (constant per element) converge more slowly than displacements.
Common mistakes
- Wrong: Solving K u = F before applying supports. — Right: K is singular until rigid-body motion is removed.
- Wrong: Trusting a single mesh. — Right: Perform a mesh-convergence study.
- Wrong: Reading stresses at nodes from linear elements as exact. — Right: Stresses are constant per element; average or refine.
Applications
- Truss analysis: Bar elements rotated into 2-D/3-D form every truss program.
- Heat conduction: Same matrices with k/h for 1-D thermal problems.
- Piping & shafts: Axial and torsional 1-D elements.
- Commercial FEM: The same assembly logic scales to millions of DOFs.
Key takeaways
- Discretise → element matrices → assemble → BCs → solve.
- kₑ = (EA/h)[[1,−1],[−1,1]].
- K is symmetric, sparse, singular until supported.
- Always check mesh convergence.
Going further
- Isoparametric elements: Shape functions on a reference element mapped with the Jacobian.
- Higher-order elements: Quadratic elements capture curved fields with fewer nodes.
- Sparse solvers: Skyline, Cholesky and iterative PCG for large K.