Article
Engineering · Numerical Methods · ⏱ ≈ 15 хв читання

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 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. This article builds a 2D plane-stress FEM solver from scratch — from elasticity fundamentals to sparse assembly and a JavaScript implementation.

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