// Float64 reference implementation of the spatial discretization. It mirrors // step.wgsl and is used for the time-step estimate, the tests, and the // GPU-versus-CPU self-test. It is not used in the time loop. import { type Grid, type Material, insideCell, insideNode, nodeWeight } from "../params.ts"; export interface Fields { u: Float64Array; v: Float64Array; w: Float64Array; } export function fields(n: number): Fields { return { u: new Float64Array(n), v: new Float64Array(n), w: new Float64Array(n) }; } const G3 = 1 / Math.sqrt(3); /** Shape function derivatives of the bilinear cell at Gauss point gp (0..3), scaled by 2/h. */ function shapeDerivatives(gp: number, hx: number, hy: number, dNdx: Float64Array, dNdy: Float64Array): void { 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); } /** * Undamped accelerations (out.u, out.v, out.w) of every node for the * displacement fields f and the stress-free shape w0. Bending forces are the * gradient of the discrete Kirchhoff energy (natural free-edge conditions); * membrane forces come from bilinear cells with 2x2 Gauss integration and * the Marguerre form of the von Karman strains. Nodes outside the sheet get zero. */ 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, hy2 = hy * hy; const nu = m.nu; const id = (i: number, j: number) => j * nx + i; // A second difference exists where the node and both of its neighbors belong to the sheet. const hasA = (i: number, j: number) => insideNode(g, i - 1, j) && insideNode(g, i, j) && insideNode(g, i + 1, j); const hasB = (i: number, j: number) => insideNode(g, i, j - 1) && insideNode(g, i, j) && insideNode(g, i, j + 1); const dxx = (i: number, j: number) => (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / hx2; const dyy = (i: number, j: number) => (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / hy2; // Weighted bending moments at nodes; zero where the second difference is undefined. const Ma = (i: number, j: number): number => { if (!hasA(i, j)) return 0; return nodeWeight(g, i, j) * (dxx(i, j) + nu * (hasB(i, j) ? dyy(i, j) : 0)); }; const Mb = (i: number, j: number): number => { if (!hasB(i, j)) return 0; return nodeWeight(g, i, j) * (dyy(i, j) + nu * (hasA(i, j) ? dxx(i, j) : 0)); }; // Mixed second difference at a cell center; zero for cells outside the sheet. const Cc = (ci: number, cj: number): number => { if (!insideCell(g, ci, cj)) 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), V = new Float64Array(4), Wn = new Float64Array(4), W0 = new Float64Array(4); const dNdx = new Float64Array(4), dNdy = new Float64Array(4); // Adds the membrane force of cell (ci, cj) on its local node a to F = [Fu, Fv, Fw]. const cellForce = (ci: number, cj: number, a: number, F: Float64Array): void => { if (!insideCell(g, ci, cj)) return; const nn = [id(ci, cj), id(ci + 1, cj), id(ci + 1, cj + 1), id(ci, cj + 1)]; for (let b = 0; b < 4; b++) { U[b] = u[nn[b]]; V[b] = v[nn[b]]; Wn[b] = w[nn[b]]; W0[b] = w0 ? w0[nn[b]] : 0; } for (let gp = 0; gp < 4; gp++) { shapeDerivatives(gp, hx, hy, dNdx, dNdy); 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]; } // Marguerre strains: total slope (w0 + w) minus the stress-free slope of w0. 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 weight = nodeWeight(g, i, j); if (weight === 0) { out.u[k] = 0; out.v[k] = 0; out.w[k] = 0; continue; } 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 * weight; out.u[k] = F[0] / mass; out.v[k] = F[1] / mass; out.w[k] = F[2] / mass; } } } /** * Adds the loss terms to the accelerations `out` for the velocities `vel`: * -2 sigma0 v on every component, +2 sigma1 (graph Laplacian of w_t) on the * transverse component, and -2 sigmaIn v on the in-plane components. */ export function addDamping(g: Grid, sigma0: number, sigma1: number, sigmaIn: number, vel: Fields, out: Fields): void { const { nx, ny, hx, hy } = g; const id = (i: number, j: number) => j * nx + i; for (let j = 0; j < ny; j++) { for (let i = 0; i < nx; i++) { const k = id(i, j); if (nodeWeight(g, i, j) === 0) continue; const c = vel.w[k]; let lap = 0; if (insideNode(g, i - 1, j)) lap += (vel.w[id(i - 1, j)] - c) / (hx * hx); if (insideNode(g, i + 1, j)) lap += (vel.w[id(i + 1, j)] - c) / (hx * hx); if (insideNode(g, i, j - 1)) lap += (vel.w[id(i, j - 1)] - c) / (hy * hy); if (insideNode(g, i, j + 1)) lap += (vel.w[id(i, j + 1)] - c) / (hy * hy); out.u[k] -= 2 * (sigma0 + sigmaIn) * vel.u[k]; out.v[k] -= 2 * (sigma0 + sigmaIn) * vel.v[k]; out.w[k] += -2 * sigma0 * c + 2 * sigma1 * lap; } } } /** Discrete potential energy (bending + membrane) whose negative gradient is the force in `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 (insideNode(g, i - 1, j) && insideNode(g, i, j) && insideNode(g, i + 1, j)) a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / (hx * hx); if (insideNode(g, i, j - 1) && insideNode(g, i, j) && insideNode(g, i, j + 1)) b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / (hy * hy); Ub += nodeWeight(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++) { if (!insideCell(g, ci, cj)) continue; 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; const dNdx = new Float64Array(4), dNdy = new Float64Array(4); for (let cj = 0; cj < ny - 1; cj++) { for (let ci = 0; ci < nx - 1; ci++) { if (!insideCell(g, ci, cj)) continue; 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++) { shapeDerivatives(gp, hx, hy, dNdx, dNdy); 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; } /** * Largest angular frequency of the linearized discrete system, by power * iteration on a small full grid with the same spacing. The extreme mode is a * grid-scale in-plane mode, so neither the sheet size nor its outline matters. * `inPlaneMass` is the factor by which the in-plane inertia is scaled. */ export function maxAngularFrequency(g: Grid, m: Material, inPlaneMass = 1, 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(n), y = fields(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); if (inPlaneMass !== 1) { for (let k = 0; k < n; k++) { y.u[k] /= inPlaneMass; y.v[k] /= inPlaneMass; } } 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)); }