// CPU reference implementation of the spatial discretization (float64). // It mirrors step.wgsl exactly and is used for the stable time-step estimate // and for the tests. It is not used in the time loop of the browser simulation. import { type Grid, type Material, trapWeight } from "../params.ts"; export interface Fields { u: Float64Array; v: Float64Array; w: Float64Array; } const G3 = 1 / Math.sqrt(3); /** * Accelerations (au, av, aw) of every node for displacement fields (u, v, w), * without damping. Bending forces come from the discrete Kirchhoff energy * (natural free-edge conditions); membrane forces come from bilinear (Q4) * elements with 2x2 Gauss integration and the von Karman strains. */ export function accelerations(g: Grid, m: Material, f: Fields, out: Fields, w0?: Float64Array): void { const { nx, ny, hx, hy } = g; const { u, v, w } = f; const hx2 = hx * hx; const hy2 = hy * hy; const nu = m.nu; const id = (i: number, j: number) => j * nx + i; // Bending moment-like quantities at nodes (zero where the second difference is undefined). const Ma = (i: number, j: number): number => { if (i < 1 || i > nx - 2 || j < 0 || j > ny - 1) return 0; const a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / hx2; let b = 0; if (j >= 1 && j <= ny - 2) b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / hy2; return trapWeight(g, i, j) * (a + nu * b); }; const Mb = (i: number, j: number): number => { if (j < 1 || j > ny - 2 || i < 0 || i > nx - 1) return 0; const b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / hy2; let a = 0; if (i >= 1 && i <= nx - 2) a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / hx2; return trapWeight(g, i, j) * (b + nu * a); }; // Mixed second difference at a cell center (zero for cells outside the plate). const Cc = (ci: number, cj: number): number => { if (ci < 0 || ci > nx - 2 || cj < 0 || cj > ny - 2) return 0; return (w[id(ci + 1, cj + 1)] - w[id(ci + 1, cj)] - w[id(ci, cj + 1)] + w[id(ci, cj)]) / (hx * hy); }; const q = (hx * hy) / 4; // area per Gauss point const U = new Float64Array(4); const V = new Float64Array(4); const Wn = new Float64Array(4); const W0 = new Float64Array(4); const dNdx = new Float64Array(4); const dNdy = new Float64Array(4); // Membrane force on local node `a` of cell (ci, cj); returns [Fu, Fv, Fw]. const cellForce = (ci: number, cj: number, a: number, F: Float64Array): void => { if (ci < 0 || ci > nx - 2 || cj < 0 || cj > ny - 2) return; const n0 = id(ci, cj), n1 = id(ci + 1, cj), n2 = id(ci + 1, cj + 1), n3 = id(ci, cj + 1); U[0] = u[n0]; U[1] = u[n1]; U[2] = u[n2]; U[3] = u[n3]; V[0] = v[n0]; V[1] = v[n1]; V[2] = v[n2]; V[3] = v[n3]; Wn[0] = w[n0]; Wn[1] = w[n1]; Wn[2] = w[n2]; Wn[3] = w[n3]; if (w0) { W0[0] = w0[n0]; W0[1] = w0[n1]; W0[2] = w0[n2]; W0[3] = w0[n3]; } for (let gp = 0; gp < 4; gp++) { const xi = gp & 1 ? G3 : -G3; const eta = gp & 2 ? G3 : -G3; dNdx[0] = (-(1 - eta) / 4) * (2 / hx); dNdx[1] = ((1 - eta) / 4) * (2 / hx); dNdx[2] = ((1 + eta) / 4) * (2 / hx); dNdx[3] = (-(1 + eta) / 4) * (2 / hx); dNdy[0] = (-(1 - xi) / 4) * (2 / hy); dNdy[1] = (-(1 + xi) / 4) * (2 / hy); dNdy[2] = ((1 + xi) / 4) * (2 / hy); dNdy[3] = ((1 - xi) / 4) * (2 / hy); // Marguerre strains: total slope (w0 + w) minus the stress-free slope of w0. let ux = 0, uy = 0, vx = 0, vy = 0, wx = 0, wy = 0, w0x = 0, w0y = 0; for (let b = 0; b < 4; b++) { ux += dNdx[b] * U[b]; uy += dNdy[b] * U[b]; vx += dNdx[b] * V[b]; vy += dNdy[b] * V[b]; wx += dNdx[b] * (Wn[b] + W0[b]); wy += dNdy[b] * (Wn[b] + W0[b]); w0x += dNdx[b] * W0[b]; w0y += dNdy[b] * W0[b]; } const exx = ux + 0.5 * (wx * wx - w0x * w0x); const eyy = vy + 0.5 * (wy * wy - w0y * w0y); const gxy = uy + vx + wx * wy - w0x * w0y; const Nxx = m.C * (exx + nu * eyy); const Nyy = m.C * (eyy + nu * exx); const Nxy = m.G * gxy; F[0] -= q * (Nxx * dNdx[a] + Nxy * dNdy[a]); F[1] -= q * (Nyy * dNdy[a] + Nxy * dNdx[a]); F[2] -= q * ((Nxx * wx + Nxy * wy) * dNdx[a] + (Nxy * wx + Nyy * wy) * dNdy[a]); } }; const F = new Float64Array(3); for (let j = 0; j < ny; j++) { for (let i = 0; i < nx; i++) { const k = id(i, j); const bend = (Ma(i - 1, j) - 2 * Ma(i, j) + Ma(i + 1, j)) / hx2 + (Mb(i, j - 1) - 2 * Mb(i, j) + Mb(i, j + 1)) / hy2 + (2 * (1 - nu) * (Cc(i, j) - Cc(i - 1, j) - Cc(i, j - 1) + Cc(i - 1, j - 1))) / (hx * hy); F[0] = 0; F[1] = 0; F[2] = -m.D * hx * hy * bend; cellForce(i, j, 0, F); cellForce(i - 1, j, 1, F); cellForce(i - 1, j - 1, 2, F); cellForce(i, j - 1, 3, F); const mass = m.rhoh * hx * hy * trapWeight(g, i, j); out.u[k] = F[0] / mass; out.v[k] = F[1] / mass; out.w[k] = F[2] / mass; } } } /** * Largest angular frequency of the linearized discrete system, by power * iteration on a small grid with the same spacing (the extreme mode is a * grid-scale mode, so the plate size does not matter). */ export function maxAngularFrequency(g: Grid, m: Material, nSmall = 12, iters = 300): number { const gs: Grid = { nx: nSmall, ny: nSmall, hx: g.hx, hy: g.hy, n: nSmall * nSmall }; const n = gs.n; const x: Fields = { u: new Float64Array(n), v: new Float64Array(n), w: new Float64Array(n) }; const y: Fields = { u: new Float64Array(n), v: new Float64Array(n), w: new Float64Array(n) }; let seed = 12345; const rnd = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296 - 0.5; }; for (let k = 0; k < n; k++) { x.u[k] = rnd(); x.v[k] = rnd(); x.w[k] = rnd(); } const eps = 1e-9; // keeps the nonlinear terms negligible let lambda = 0; for (let it = 0; it < iters; it++) { let norm = 0; for (let k = 0; k < n; k++) norm += x.u[k] ** 2 + x.v[k] ** 2 + x.w[k] ** 2; norm = Math.sqrt(norm); for (let k = 0; k < n; k++) { x.u[k] *= eps / norm; x.v[k] *= eps / norm; x.w[k] *= eps / norm; } accelerations(gs, m, x, y); let dot = 0; for (let k = 0; k < n; k++) dot += x.u[k] * y.u[k] + x.v[k] * y.v[k] + x.w[k] * y.w[k]; lambda = -dot / (eps * eps); // Rayleigh quotient of -M^{-1}K for (let k = 0; k < n; k++) { x.u[k] = -y.u[k]; x.v[k] = -y.v[k]; x.w[k] = -y.w[k]; } } return Math.sqrt(Math.max(lambda, 0)); } /** Discrete potential energy (bending + membrane) matching `accelerations`. */ export function potentialEnergy(g: Grid, m: Material, f: Fields, w0?: Float64Array): number { const { nx, ny, hx, hy } = g; const { u, v, w } = f; const nu = m.nu; const id = (i: number, j: number) => j * nx + i; let Ub = 0; for (let j = 0; j < ny; j++) { for (let i = 0; i < nx; i++) { let a = 0, b = 0; if (i >= 1 && i <= nx - 2) a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / (hx * hx); if (j >= 1 && j <= ny - 2) b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / (hy * hy); Ub += trapWeight(g, i, j) * (a * a + b * b + 2 * nu * a * b); } } for (let cj = 0; cj < ny - 1; cj++) { for (let ci = 0; ci < nx - 1; ci++) { const c = (w[id(ci + 1, cj + 1)] - w[id(ci + 1, cj)] - w[id(ci, cj + 1)] + w[id(ci, cj)]) / (hx * hy); Ub += 2 * (1 - nu) * c * c; } } Ub *= 0.5 * m.D * hx * hy; let Um = 0; const q = (hx * hy) / 4; for (let cj = 0; cj < ny - 1; cj++) { for (let ci = 0; ci < nx - 1; ci++) { const nn = [id(ci, cj), id(ci + 1, cj), id(ci + 1, cj + 1), id(ci, cj + 1)]; for (let gp = 0; gp < 4; gp++) { const xi = gp & 1 ? G3 : -G3; const eta = gp & 2 ? G3 : -G3; const dNdx = [-(1 - eta), 1 - eta, 1 + eta, -(1 + eta)].map((s) => (s / 4) * (2 / hx)); const dNdy = [-(1 - xi), -(1 + xi), 1 + xi, 1 - xi].map((s) => (s / 4) * (2 / hy)); let ux = 0, uy = 0, vx = 0, vy = 0, wx = 0, wy = 0, w0x = 0, w0y = 0; for (let b = 0; b < 4; b++) { const z0 = w0 ? w0[nn[b]] : 0; ux += dNdx[b] * u[nn[b]]; uy += dNdy[b] * u[nn[b]]; vx += dNdx[b] * v[nn[b]]; vy += dNdy[b] * v[nn[b]]; wx += dNdx[b] * (w[nn[b]] + z0); wy += dNdy[b] * (w[nn[b]] + z0); w0x += dNdx[b] * z0; w0y += dNdy[b] * z0; } const exx = ux + 0.5 * (wx * wx - w0x * w0x); const eyy = vy + 0.5 * (wy * wy - w0y * w0y); const gxy = uy + vx + wx * wy - w0x * w0y; Um += q * 0.5 * (m.C * (exx * exx + eyy * eyy + 2 * nu * exx * eyy) + m.G * gxy * gxy); } } } return Ub + Um; }