1// CPU reference implementation of the spatial discretization (float64).
2// It mirrors step.wgsl exactly and is used for the stable time-step estimate
3// and for the tests. It is not used in the time loop of the browser simulation.
5import { type Grid, type Material, trapWeight } from "../params.ts";
7export interface Fields {
8 u: Float64Array;
9 v: Float64Array;
10 w: Float64Array;
11}
13const G3 = 1 / Math.sqrt(3);
15/**
16 * Accelerations (au, av, aw) of every node for displacement fields (u, v, w),
17 * without damping. Bending forces come from the discrete Kirchhoff energy
18 * (natural free-edge conditions); membrane forces come from bilinear (Q4)
19 * elements with 2x2 Gauss integration and the von Karman strains.
20 */
21export function accelerations(g: Grid, m: Material, f: Fields, out: Fields): void {
22 const { nx, ny, hx, hy } = g;
23 const { u, v, w } = f;
24 const hx2 = hx * hx;
25 const hy2 = hy * hy;
26 const nu = m.nu;
27 const id = (i: number, j: number) => j * nx + i;
29 // Bending moment-like quantities at nodes (zero where the second difference is undefined).
30 const Ma = (i: number, j: number): number => {
31 if (i < 1 || i > nx - 2 || j < 0 || j > ny - 1) return 0;
32 const a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / hx2;
33 let b = 0;
34 if (j >= 1 && j <= ny - 2) b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / hy2;
35 return trapWeight(g, i, j) * (a + nu * b);
36 };
37 const Mb = (i: number, j: number): number => {
38 if (j < 1 || j > ny - 2 || i < 0 || i > nx - 1) return 0;
39 const b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / hy2;
40 let a = 0;
41 if (i >= 1 && i <= nx - 2) a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / hx2;
42 return trapWeight(g, i, j) * (b + nu * a);
43 };
44 // Mixed second difference at a cell center (zero for cells outside the plate).
45 const Cc = (ci: number, cj: number): number => {
46 if (ci < 0 || ci > nx - 2 || cj < 0 || cj > ny - 2) return 0;
47 return (w[id(ci + 1, cj + 1)] - w[id(ci + 1, cj)] - w[id(ci, cj + 1)] + w[id(ci, cj)]) / (hx * hy);
48 };
50 const q = (hx * hy) / 4; // area per Gauss point
51 const U = new Float64Array(4);
52 const V = new Float64Array(4);
53 const Wn = new Float64Array(4);
54 const dNdx = new Float64Array(4);
55 const dNdy = new Float64Array(4);
57 // Membrane force on local node `a` of cell (ci, cj); returns [Fu, Fv, Fw].
58 const cellForce = (ci: number, cj: number, a: number, F: Float64Array): void => {
59 if (ci < 0 || ci > nx - 2 || cj < 0 || cj > ny - 2) return;
60 const n0 = id(ci, cj), n1 = id(ci + 1, cj), n2 = id(ci + 1, cj + 1), n3 = id(ci, cj + 1);
61 U[0] = u[n0]; U[1] = u[n1]; U[2] = u[n2]; U[3] = u[n3];
62 V[0] = v[n0]; V[1] = v[n1]; V[2] = v[n2]; V[3] = v[n3];
63 Wn[0] = w[n0]; Wn[1] = w[n1]; Wn[2] = w[n2]; Wn[3] = w[n3];
64 for (let gp = 0; gp < 4; gp++) {
65 const xi = gp & 1 ? G3 : -G3;
66 const eta = gp & 2 ? G3 : -G3;
67 dNdx[0] = (-(1 - eta) / 4) * (2 / hx);
68 dNdx[1] = ((1 - eta) / 4) * (2 / hx);
69 dNdx[2] = ((1 + eta) / 4) * (2 / hx);
70 dNdx[3] = (-(1 + eta) / 4) * (2 / hx);
71 dNdy[0] = (-(1 - xi) / 4) * (2 / hy);
72 dNdy[1] = (-(1 + xi) / 4) * (2 / hy);
73 dNdy[2] = ((1 + xi) / 4) * (2 / hy);
74 dNdy[3] = ((1 - xi) / 4) * (2 / hy);
75 let ux = 0, uy = 0, vx = 0, vy = 0, wx = 0, wy = 0;
76 for (let b = 0; b < 4; b++) {
77 ux += dNdx[b] * U[b]; uy += dNdy[b] * U[b];
78 vx += dNdx[b] * V[b]; vy += dNdy[b] * V[b];
79 wx += dNdx[b] * Wn[b]; wy += dNdy[b] * Wn[b];
80 }
81 const exx = ux + 0.5 * wx * wx;
82 const eyy = vy + 0.5 * wy * wy;
83 const gxy = uy + vx + wx * wy;
84 const Nxx = m.C * (exx + nu * eyy);
85 const Nyy = m.C * (eyy + nu * exx);
86 const Nxy = m.G * gxy;
87 F[0] -= q * (Nxx * dNdx[a] + Nxy * dNdy[a]);
88 F[1] -= q * (Nyy * dNdy[a] + Nxy * dNdx[a]);
89 F[2] -= q * ((Nxx * wx + Nxy * wy) * dNdx[a] + (Nxy * wx + Nyy * wy) * dNdy[a]);
90 }
91 };
93 const F = new Float64Array(3);
94 for (let j = 0; j < ny; j++) {
95 for (let i = 0; i < nx; i++) {
96 const k = id(i, j);
97 const bend =
98 (Ma(i - 1, j) - 2 * Ma(i, j) + Ma(i + 1, j)) / hx2 +
99 (Mb(i, j - 1) - 2 * Mb(i, j) + Mb(i, j + 1)) / hy2 +
100 (2 * (1 - nu) * (Cc(i, j) - Cc(i - 1, j) - Cc(i, j - 1) + Cc(i - 1, j - 1))) / (hx * hy);
101 F[0] = 0; F[1] = 0; F[2] = -m.D * hx * hy * bend;
102 cellForce(i, j, 0, F);
103 cellForce(i - 1, j, 1, F);
104 cellForce(i - 1, j - 1, 2, F);
105 cellForce(i, j - 1, 3, F);
106 const mass = m.rhoh * hx * hy * trapWeight(g, i, j);
107 out.u[k] = F[0] / mass;
108 out.v[k] = F[1] / mass;
109 out.w[k] = F[2] / mass;
110 }
111 }
112}
114/**
115 * Largest angular frequency of the linearized discrete system, by power
116 * iteration on a small grid with the same spacing (the extreme mode is a
117 * grid-scale mode, so the plate size does not matter).
118 */
119export function maxAngularFrequency(g: Grid, m: Material, nSmall = 12, iters = 300): number {
120 const gs: Grid = { nx: nSmall, ny: nSmall, hx: g.hx, hy: g.hy, n: nSmall * nSmall };
121 const n = gs.n;
122 const x: Fields = { u: new Float64Array(n), v: new Float64Array(n), w: new Float64Array(n) };
123 const y: Fields = { u: new Float64Array(n), v: new Float64Array(n), w: new Float64Array(n) };
124 let seed = 12345;
125 const rnd = () => {
126 seed = (seed * 1664525 + 1013904223) >>> 0;
127 return seed / 4294967296 - 0.5;
128 };
129 for (let k = 0; k < n; k++) {
130 x.u[k] = rnd(); x.v[k] = rnd(); x.w[k] = rnd();
131 }
132 const eps = 1e-9; // keeps the nonlinear terms negligible
133 let lambda = 0;
134 for (let it = 0; it < iters; it++) {
135 let norm = 0;
136 for (let k = 0; k < n; k++) norm += x.u[k] ** 2 + x.v[k] ** 2 + x.w[k] ** 2;
137 norm = Math.sqrt(norm);
138 for (let k = 0; k < n; k++) {
139 x.u[k] *= eps / norm; x.v[k] *= eps / norm; x.w[k] *= eps / norm;
140 }
141 accelerations(gs, m, x, y);
142 let dot = 0;
143 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];
144 lambda = -dot / (eps * eps); // Rayleigh quotient of -M^{-1}K
145 for (let k = 0; k < n; k++) {
146 x.u[k] = -y.u[k]; x.v[k] = -y.v[k]; x.w[k] = -y.w[k];
147 }
148 }
149 return Math.sqrt(Math.max(lambda, 0));
150}
152/** Discrete potential energy (bending + membrane) matching `accelerations`. */
153export function potentialEnergy(g: Grid, m: Material, f: Fields): number {
154 const { nx, ny, hx, hy } = g;
155 const { u, v, w } = f;
156 const nu = m.nu;
157 const id = (i: number, j: number) => j * nx + i;
158 let Ub = 0;
159 for (let j = 0; j < ny; j++) {
160 for (let i = 0; i < nx; i++) {
161 let a = 0, b = 0;
162 if (i >= 1 && i <= nx - 2) a = (w[id(i + 1, j)] - 2 * w[id(i, j)] + w[id(i - 1, j)]) / (hx * hx);
163 if (j >= 1 && j <= ny - 2) b = (w[id(i, j + 1)] - 2 * w[id(i, j)] + w[id(i, j - 1)]) / (hy * hy);
164 Ub += trapWeight(g, i, j) * (a * a + b * b + 2 * nu * a * b);
165 }
166 }
167 for (let cj = 0; cj < ny - 1; cj++) {
168 for (let ci = 0; ci < nx - 1; ci++) {
169 const c = (w[id(ci + 1, cj + 1)] - w[id(ci + 1, cj)] - w[id(ci, cj + 1)] + w[id(ci, cj)]) / (hx * hy);
170 Ub += 2 * (1 - nu) * c * c;
171 }
172 }
173 Ub *= 0.5 * m.D * hx * hy;
175 let Um = 0;
176 const q = (hx * hy) / 4;
177 for (let cj = 0; cj < ny - 1; cj++) {
178 for (let ci = 0; ci < nx - 1; ci++) {
179 const nn = [id(ci, cj), id(ci + 1, cj), id(ci + 1, cj + 1), id(ci, cj + 1)];
180 for (let gp = 0; gp < 4; gp++) {
181 const xi = gp & 1 ? G3 : -G3;
182 const eta = gp & 2 ? G3 : -G3;
183 const dNdx = [-(1 - eta), 1 - eta, 1 + eta, -(1 + eta)].map((s) => (s / 4) * (2 / hx));
184 const dNdy = [-(1 - xi), -(1 + xi), 1 + xi, 1 - xi].map((s) => (s / 4) * (2 / hy));
185 let ux = 0, uy = 0, vx = 0, vy = 0, wx = 0, wy = 0;
186 for (let b = 0; b < 4; b++) {
187 ux += dNdx[b] * u[nn[b]]; uy += dNdy[b] * u[nn[b]];
188 vx += dNdx[b] * v[nn[b]]; vy += dNdy[b] * v[nn[b]];
189 wx += dNdx[b] * w[nn[b]]; wy += dNdy[b] * w[nn[b]];
190 }
191 const exx = ux + 0.5 * wx * wx;
192 const eyy = vy + 0.5 * wy * wy;
193 const gxy = uy + vx + wx * wy;
194 Um += q * 0.5 * (m.C * (exx * exx + eyy * eyy + 2 * nu * exx * eyy) + m.G * gxy * gxy);
195 }
196 }
197 }
198 return Ub + Um;
199}