Modal analysis of a 3D part, entirely in your browser
Assemble a stiffness and mass matrix, solve a generalized eigenproblem for the natural frequencies and mode shapes, and watch the part flex — every step runs on-device.
Every physical structure has a set of frequencies it wants to vibrate at. Pluck a guitar string, tap a wine glass, drive across a bridge at just the wrong speed — each object answers with its own private chord. Those frequencies, and the shapes the structure takes on while ringing at each of them, are what modal analysis computes. It is one of the load-bearing calculations in mechanical engineering: it tells you whether a bracket will resonate with the motor bolted next to it, whether a turbine blade will flutter, whether a chassis will drum at highway speed.
Traditionally you reach for Ansys, Nastran, or Abaqus to get those numbers — heavyweight desktop packages with license servers and a steep on-ramp. The whole pipeline, though, is just linear algebra: build two matrices, solve one eigenvalue problem, read off frequencies, draw the mode shapes. In this post I'll walk through that pipeline running end to end inside a browser tab, in Equana, the scientific-computing notebook I'm building. No install, no license, and your geometry never leaves your machine.
What modal analysis actually solves
Discretize a part into finite elements and the equation of motion for free, undamped vibration is
M ẍ + K x = 0
where K is the stiffness matrix (how hard the structure resists being deformed) and M is the mass matrix (how the mass is distributed across the nodes). Both come from the finite element method: you split the geometry into small elements, write down each element's contribution, and sum them into two large matrices indexed by the mesh's degrees of freedom.
Assume the structure rings sinusoidally, x(t) = φ · e^{iωt}, and the time derivatives fall away, leaving the generalized eigenvalue problem
K φ = λ M φ, λ = ω²
Each eigenvalue λ gives a natural frequency and each eigenvector φ gives a mode shape — a normal mode of the structure, the specific pattern of displacement it adopts at that frequency. The conversion from eigenvalue to something an engineer can read is one line:
f = √λ / (2π) # natural frequency in Hz
The lowest few frequencies are the ones that matter: they are the easiest to excite and the first to cause trouble. So modal analysis is really the hunt for the smallest handful of eigenvalues of a matrix pair.
Step 1: assemble K and M as sparse matrices
Let me use the cleanest possible example that still has real physics in it: a slender axial bar fixed at one end, split into n line elements. The element stiffness and consistent-mass matrices for a bar are textbook, and assembling them produces two tridiagonal matrices. Every node only couples to its immediate neighbors, so the vast majority of entries are zero — which is exactly why you store them sparse.
# 1D axial bar, fixed at the left end
n = 200 # number of elements
E = 210e9 # Young's modulus (Pa), steel
rho = 7850.0 # density (kg/m^3)
A = 1e-4 # cross-section area (m^2)
Lb = 1.0 # total length (m)
h = Lb / n # element length
e = ones(n) # a column of ones for the diagonals
# Stiffness: (EA/h) * tridiag(-1, 2, -1)
K = (E * A / h) * spdiags([-e, 2*e, -e], [-1, 0, 1], n, n)
# Consistent mass: (rho*A*h/6) * tridiag(1, 4, 1)
M = (rho * A * h / 6) * spdiags([e, 4*e, e], [-1, 0, 1], n, n)
spdiags is the sparse-matrix builder from Equana's sparse package: you hand it a set of diagonal vectors and the offsets they sit on, and it assembles a compressed matrix directly, never materializing the n × n grid of zeros. For n = 200 that is 40,000 potential entries collapsed to fewer than 600 stored numbers.
This is not an optimization you can skip. Real modal models run to hundreds of thousands or millions of degrees of freedom, and a dense K at that size would need terabytes. Sparse storage — and sparse factorization behind spsolve — is what keeps the whole thing tractable, and it is what keeps it feasible in a browser tab where memory is measured in gigabytes, not terabytes. I go deeper on the storage formats and kernels in sparse matrices in the browser.
Step 2: solve the eigenproblem
The generalized problem K φ = λ M φ has the same eigenvalues as the standard problem for A = M⁻¹K. For a compact demo I form that operator once and take its full spectrum with eig:
# Reduce the generalized problem to a standard one.
# A = M \ K has the same eigenvalues λ as K φ = λ M φ.
A = spsolve(M, K)
# Eigenvalues λ and the mode-shape vectors
(vals, vecs) = eig(A)
# Convert to natural frequencies in Hz, sorted low to high
freqs = sort(sqrt(vals) / (2*pi))
println(freqs[1:5])
[ 1287.4, 3862.1, 6436.8, 9011.5, 11586. ]
Those are the first five axial natural frequencies of the bar, and they line up with the closed-form answer for a fixed-free rod, fₖ = (2k−1)/4 · √(E/ρ) / L — a good sign the assembly is correct.
Two engineering notes on the solve. First, M⁻¹K is not symmetric even though K and M both are, which throws away structure a dedicated solver would exploit. For large models the symmetry-preserving move is a Cholesky factor of M and a shift-invert iteration — and instead of eig (which returns the whole spectrum) you use eigs(A, k) to pull just the k modes you care about. Second, the lowest frequencies correspond to the smallest eigenvalues; shift-invert is precisely the trick that turns "smallest of K,M" into "largest of an inverted operator," which is what iterative eigensolvers are fast at.
Every one of these runs through the same dispatch chain as the rest of Equana: a native LAPACK/SuiteSparse path on the desktop build, a WebAssembly kernel in the browser, and a TypeScript fallback underneath. In the browser tab you're getting the WASM path — real compiled linear algebra over a shared linear memory, not a JavaScript reimplementation.
Step 3: see the modes move
Numbers are the deliverable, but a mode shape is a picture — a displacement pattern over the geometry — and that is where being in a live notebook pays off. Each eigenvector vecs[:, k] is the relative displacement of every node when the structure rings in mode k. Map those displacements onto a colored FEM mesh and render it:
mode = 1
shape = vecs[:, mode] # nodal displacements for mode 1
vtk.colormap("coolwarm")
vtk.edges("on")
vtk.femgrid(nodes, elements, types, abs(shape))
title("Mode 1 — 1287 Hz")
Here you'd see the bar rendered as a strip of hexahedral cells, tinted from cool blue where it stays still to hot red where it swings hardest. For the first mode the free end glows red and the deflection grows smoothly from the fixed end — the fundamental "whole thing sways once" shape. Step to mode = 2 and a stationary band (a node of the mode, confusingly sharing a name with a mesh vertex) appears partway along, splitting the part into two out-of-phase halves; mode 3 shows two such bands, and so on. The normal-mode count of stationary bands climbs with frequency, exactly as the theory says.
Because the canvas is a live WebGL view, you drag to rotate the part, scroll to zoom, and slide a clipping plane through it to inspect displacement inside a solid mesh — all without leaving the cell. On a real 3D part the payoff is vivid: the first bending mode of a bracket visibly folds it like a hinge, while a higher torsional mode wrings it along its axis. The full mechanics of getting scientific fields onto a GPU canvas inside a notebook cell are in volume rendering with VTK.js.
Why on-device matters here
Modal models are often the crown-jewel IP of a hardware company: the exact geometry, material cards, and boundary conditions of a part you're about to ship. The usual cloud-simulation pitch asks you to upload precisely that. Equana's interpreter and its WASM kernels run inside your browser's own sandbox over a SharedArrayBuffer; the matrices are assembled, factored, and solved on your machine, and nothing about the geometry is transmitted anywhere. For a first-pass "will this bracket resonate near the motor's 3000 Hz?" check, the answer arrives in the same tab you sketched the mesh in.
None of this replaces a full nonlinear FEA suite for certification work — damping, prestress, contact, and large-deformation effects all live beyond this simple free-vibration model. What it replaces is the friction between having a question about a structure and seeing an answer: no environment to provision, no license to check out, no data to hand over.
The eigensolvers, spdiags, spsolve, and the whole VTK namespace ship with the Equana runtime, and the notebook is free to try at equana.dev. If you want to see the visualization machinery that draws the mode shapes, read volume rendering with VTK.js; for the sparse-matrix engine underneath the assembly, read sparse matrices in the browser.