Article
Engineering · Numerical Methods · ⏱ ~15 min read · Last updated: 22 June 2026

Finite Element Method — Structural Analysis from First Principles

The Finite Element Method (FEM) is the computational backbone of virtually all modern structural, thermal, and fluid engineering. It transforms a continuous PDE (partial differential equation) over a complex domain into a large sparse linear system by subdividing the domain into small elements, approximating the solution with polynomial shape functions, and applying the Galerkin weak form (a technique that rewrites the equations in integral form so they can be solved approximately using simple basis functions). This article builds a 2D plane-stress FEM solver from scratch — from elasticity fundamentals to sparse assembly and a JavaScript implementation.

TL;DR: This article builds a 2D plane-stress finite element solver from first principles: deriving the elasticity equations, converting them into a weak (Galerkin) form, assembling element stiffness matrices from triangular shape functions, applying boundary conditions, and recovering von Mises stress — ending with a working JavaScript CST-element solver and a comparison of element types used in commercial FEM software.

1. Linear Elasticity and Governing Equations

Linear elasticity assumes small strains, linear material response (Hooke's law), and no plastic deformation. The equilibrium PDE in 3D is:

Equilibrium: ∂σ_ij/∂x_j + f_i = 0 (sum on j) Strain–displacement: ε_xx = ∂u/∂x, ε_yy = ∂v/∂y, γ_xy = ∂u/∂y + ∂v/∂x Hooke's law (plane stress, 2D): { σ_xx } E/(1−ν²) · [1 ν 0 ] { ε_xx } { σ_yy } = [ν 1 0 ] { ε_yy } { τ_xy } [0 0 (1−ν)/2 ] { γ_xy } D = E/(1−ν²) [1 ν 0; ν 1 0; 0 0 (1−ν)/2] Where: E = Young's modulus, ν = Poisson's ratio (≈ 0.3 steel)

2. Weak Form and Galerkin Method

Strong form: find u such that ∇·σ + f = 0 on Ω with u = ū on Γ_D (Dirichlet) and σ·n = t̄ on Γ_N (Neumann) Multiply by test function v, integrate by parts: ∫_Ω ε(v)^T σ dΩ = ∫_Ω v·f dΩ + ∫_Γ_N v·t̄ dΓ Galerkin: approximate u ≈ Σ u_I · N_I(x) (same basis for u and v) N_I = shape function for node I Matrix form: K · u = f K_IJ = ∫_Ω B_I^T D B_J dΩ (stiffness matrix) f_I = ∫_Ω N_I b dΩ + ∫_Γ_N N_I t̄ dΓ (force vector) B_I = [∂N_I/∂x 0 ] (strain–displacement operator per node) [0 ∂N_I/∂y ] [∂N_I/∂y ∂N_I/∂x ]

3. Shape Functions and Isoparametric Elements

CST (Constant Strain Triangle): simplest 2D element, 3 nodes Area coordinates: N₁=ξ, N₂=η, N₃=1−ξ−η Strain constant inside element (hence CST) LST (Linear Strain Triangle): 6 nodes (adds midpoints), linear strain Q4 (4-node quadrilateral): bilinear shape functions N₁=(1−ξ)(1−η)/4, N₂=(1+ξ)(1−η)/4 N₃=(1+ξ)(1+η)/4, N₄=(1−ξ)(1+η)/4 (ξ,η ∈ [−1,1]) Isoparametric mapping: same N functions map (ξ,η) → (x,y) x = Σ N_I(ξ,η) · x_I Jacobian: J = ∂(x,y)/∂(ξ,η) → B = J⁻¹ · ∂N/∂(ξ,η) Gauss quadrature for integration: ∫ f(ξ,η)|J|dξdη ≈ Σ_g w_g · f(ξ_g, η_g) · |J(ξ_g,η_g)| 2×2 Gauss: ξ=±1/√3, w=1 (exact for Q4 linear problems)

4. Element Stiffness Matrix K_e

For CST element with nodes (x₁,y₁),(x₂,y₂),(x₃,y₃): Area A = ½|det[x₂−x₁ y₂−y₁; x₃−x₁ y₃−y₁]| b₁ = y₂−y₃, b₂ = y₃−y₁, b₃ = y₁−y₂ c₁ = x₃−x₂, c₂ = x₁−x₃, c₃ = x₂−x₁ B = (1/2A) [b₁ 0 b₂ 0 b₃ 0 ] (3×6 matrix) [0 c₁ 0 c₂ 0 c₃ ] [c₁ b₁ c₂ b₂ c₃ b₃ ] K_e = B^T · D · B · A · t (6×6 element stiffness, t=thickness) Nodal DOFs: each node has 2: [u₁ v₁ u₂ v₂ u₃ v₃] → K_e is 6×6 for a 2D triangle For Q4: K_e = ∫₋₁¹∫₋₁¹ B(ξ,η)^T D B(ξ,η)|J| dξdη → 8×8

5. Global Assembly and Boundary Conditions

Assembly: scatter K_e values into global K using element connectivity For each element e, each pair of nodes (I,J): K[dof_I][dof_J] += K_e[i][j] (add contributions) Global K is sparse, symmetric, positive semi-definite DOF numbering: node i → global DOFs 2i, 2i+1 (for x, y) N nodes → 2N × 2N global stiffness matrix Dirichlet BC (fixed displacement = 0): For constrained DOF d: set K[d][d] = 1, K[d][j] = K[i][d] = 0, f[d] = ū_d (penalty method: add large K p to diagonal instead — simpler to implement) Solve: K u = f Direct: Cholesky factorisation (LAPACK dpotf2) for symmetric PD systems Iterative: Conjugate Gradient (PCG) for very large systems

6. Stress Recovery and Von Mises

Element stress from solution u: σ_e = D · B · u_e (constant per CST element) Von Mises equivalent stress (yield criterion): σ_VM = √(σ_xx² − σ_xx·σ_yy + σ_yy² + 3τ_xy²) Yield when σ_VM ≥ σ_yield (250 MPa for structural steel) Nodal stress averaging: each node shared by multiple elements σ_node = (1/V_node) Σ_e V_e · σ_e (volume-weighted average) Error estimator (Zienkiewicz-Zhu): η_e = ||σ_e − σ*_e|| / ||σ*|| where σ* is smoothed stress η > threshold → refine element (h-refinement or p-refinement) Convergence: CST needs fine mesh near stress concentrations Stress error ~ h^(p) where p=1 for CST, p=2 for LST

7. JavaScript CST Element Solver

// Minimal 2D plane-stress FEM with CST triangles
class FEM2D {
  constructor(nodes, elements, E, nu, thickness = 1) {
    this.nodes    = nodes;    // [[x,y], ...]
    this.elements = elements; // [[n0,n1,n2], ...]
    this.t = thickness;
    this.D = planeStressD(E, nu);
    const ndof = 2 * nodes.length;
    this.K = Array.from({length: ndof}, () => new Float64Array(ndof));
    this.f = new Float64Array(ndof);
  }

  assemble() {
    for (const [n0, n1, n2] of this.elements) {
      const [Ke, Be, A] = cstElement(this.nodes[n0], this.nodes[n1],
                                         this.nodes[n2], this.D, this.t);
      const dofs = [2*n0, 2*n0+1, 2*n1, 2*n1+1, 2*n2, 2*n2+1];
      for (let i = 0; i < 6; i++)
        for (let j = 0; j < 6; j++)
          this.K[dofs[i]][dofs[j]] += Ke[i][j];
    }
  }

  applyFixed(nodeIndex, axis) { // axis: 0=x, 1=y
    const d = 2 * nodeIndex + axis;
    const n = this.K.length;
    for (let j = 0; j < n; j++) this.K[d][j] = this.K[j][d] = 0;
    this.K[d][d] = 1;
    this.f[d] = 0;
  }

  applyForce(nodeIndex, fx, fy) {
    this.f[2*nodeIndex]   += fx;
    this.f[2*nodeIndex+1] += fy;
  }

  solve() { // Naive Gaussian elimination (use sparse solver in production)
    const A = this.K.map(r => Array.from(r));
    const b = Array.from(this.f);
    const n = b.length;
    for (let p = 0; p < n; p++) {
      for (let i = p+1; i < n; i++) {
        const f = A[i][p] / A[p][p];
        for (let j = p; j < n; j++) A[i][j] -= f*A[p][j];
        b[i] -= f*b[p];
      }
    }
    const u = new Float64Array(n);
    for (let i = n-1; i >= 0; i--) {
      u[i] = b[i];
      for (let j = i+1; j < n; j++) u[i] -= A[i][j]*u[j];
      u[i] /= A[i][i];
    }
    return u;
  }
}

function planeStressD(E, nu) {
  const c = E / (1 - nu*nu);
  return [[c, c*nu, 0], [c*nu, c, 0], [0, 0, c*(1-nu)/2]];
}

function cstElement([x1,y1], [x2,y2], [x3,y3], D, t) {
  const A = 0.5 * Math.abs((x2-x1)*(y3-y1) - (x3-x1)*(y2-y1));
  const b = [y2-y3, y3-y1, y1-y2];
  const c = [x3-x2, x1-x3, x2-x1];
  // B is 3×6
  const B = [
    [b[0],0,  b[1],0,  b[2],0  ],
    [0,  c[0],0,  c[1],0,  c[2]],
    [c[0],b[0],c[1],b[1],c[2],b[2]]
  ].map(row => row.map(v => v / (2*A)));
  // Ke = B^T D B A t  (6×6)
  const DB = matMul(D, B);  // 3×6
  const Ke = matMul(transpose(B), DB).map(row => row.map(v => v*A*t));
  return [Ke, B, A];
}

8. Element Types and Software

CST (T3)

3-node triangle, constant strain. Simple to implement, needs very fine mesh. Good for learning FEM.

LST (T6)

6-node triangle, linear strain. Better accuracy per element. Preferred for curved geometries.

Q4 / Q8

4- or 8-node quadrilateral. Q8 is the workhorse of commercial FEM: second-order accuracy, Gauss integration.

Hex20 (3D)

20-node hexahedron used in Abaqus/Ansys for 3D solid mechanics. Most accurate per DOF ratio.

Commercial FEM codes — Ansys, Abaqus, NASTRAN, LS-DYNA — add contact mechanics, nonlinear materials, geometric nonlinearity, dynamic (modal and transient) analysis, and multiphysics coupling. Open-source alternatives include FEniCS (Python, FEniCS forms language) and Code_Aster (general) and deal.II (C++ templates for high-order adaptive methods).