2004981Plate dynamics lab 2: a hammer on a hung plate by Rayleigh-Ritz and by Mindlin-Reissner finite elementsJeremy Magland 1import {
2 isPlainCell,
3 isPlainStruct,
4 isPlainTensor,
5 type PlainStruct,
6 type PlainValue,
7} from "../runner/plain";
8import type { Field2D, FieldFrames } from "../fields/field";
9import type { MethodId } from "../spec/types";
10import type { ContactResult, MethodPlan, ModesResult, PlateResult, ResultInfo } from "./types";
12export class ResultShapeError extends Error {}
14function field(s: PlainStruct, name: string): PlainValue {
15 const v = s.fields[name];
16 if (v === undefined) throw new ResultShapeError(`result is missing field "${name}"`);
17 return v;
18}
20function struct(s: PlainStruct, name: string): PlainStruct {
21 const v = field(s, name);
22 if (!isPlainStruct(v)) throw new ResultShapeError(`field "${name}" should be a struct`);
23 return v;
24}
26function num(s: PlainStruct, name: string): number {
27 const v = field(s, name);
28 if (typeof v === "number") return v;
29 if (typeof v === "boolean") return v ? 1 : 0;
30 if (isPlainTensor(v) && v.data.length === 1) return v.data[0];
31 throw new ResultShapeError(`field "${name}" should be a scalar`);
32}
34function numOpt(s: PlainStruct, name: string, fallback: number): number {
35 return s.fields[name] === undefined ? fallback : num(s, name);
36}
38function str(s: PlainStruct, name: string): string {
39 const v = field(s, name);
40 if (typeof v === "string") return v;
41 throw new ResultShapeError(`field "${name}" should be a string`);
42}
44function vec(s: PlainStruct, name: string): Float64Array {
45 const v = field(s, name);
46 if (typeof v === "number") return Float64Array.of(v);
47 if (isPlainTensor(v)) return v.data;
48 throw new ResultShapeError(`field "${name}" should be a numeric vector`);
49}
51function field2d(s: PlainStruct, name: string): Field2D {
52 const v = field(s, name);
53 if (!isPlainTensor(v) || v.shape.length !== 2)
54 throw new ResultShapeError(`field "${name}" should be a 2-D array`);
55 return { ny: v.shape[0], nx: v.shape[1], data: v.data };
56}
58function frames3d(s: PlainStruct, name: string): FieldFrames {
59 const v = field(s, name);
60 if (!isPlainTensor(v)) throw new ResultShapeError(`field "${name}" should be a 3-D array`);
61 const [ny, nx, nf] = [v.shape[0] ?? 0, v.shape[1] ?? 0, v.shape[2] ?? 1];
62 if (v.shape.length < 2 || v.shape.length > 3)
63 throw new ResultShapeError(`field "${name}" should be a 3-D array`);
64 return { ny, nx, nf, data: v.data };
65}
67function strings(s: PlainStruct, name: string): string[] {
68 const v = s.fields[name];
69 if (v === undefined) return [];
70 if (typeof v === "string") return [v];
71 if (isPlainCell(v)) return v.items.map((x) => (typeof x === "string" ? x : JSON.stringify(x)));
72 return [];
73}
75function methodId(s: string): MethodId {
76 if (s === "rr" || s === "fem") return s;
77 throw new ResultShapeError(`unknown method "${s}"`);
78}
80const INFO_STANDARD = new Set([
81 "method",
82 "ndof",
83 "nmodes",
84 "n_audible",
85 "n_rigid",
86 "fs",
87 "nsamples",
88 "nframes",
89 "frame_span",
90 "plate_mass",
91 "thread_k",
92 "critical_frequency",
93 "f_max",
94 "size_note",
95 "modes_wall_s",
96 "wall_s",
97 "strike_wall_s",
98 "notes",
99]);
101function parseInfo(s: PlainStruct): ResultInfo {
102 const extra: Record<string, number | string | boolean> = {};
103 for (const [k, v] of Object.entries(s.fields)) {
104 if (INFO_STANDARD.has(k)) continue;
105 if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") extra[k] = v;
106 else if (isPlainTensor(v) && v.data.length === 1) extra[k] = v.data[0];
107 }
108 return {
109 method: methodId(str(s, "method")),
110 ndof: num(s, "ndof"),
111 nmodes: num(s, "nmodes"),
112 n_audible: num(s, "n_audible"),
113 n_rigid: num(s, "n_rigid"),
114 fs: num(s, "fs"),
115 nsamples: num(s, "nsamples"),
116 nframes: num(s, "nframes"),
117 frame_span: num(s, "frame_span"),
118 plate_mass: num(s, "plate_mass"),
119 thread_k: num(s, "thread_k"),
120 critical_frequency: num(s, "critical_frequency"),
121 f_max: num(s, "f_max"),
122 size_note: str(s, "size_note"),
123 modes_wall_s: num(s, "modes_wall_s"),
124 wall_s: num(s, "wall_s"),
125 strike_wall_s: numOpt(s, "strike_wall_s", 0),
126 notes: strings(s, "notes"),
127 extra,
128 };
129}
131function parseModes(s: PlainStruct): ModesResult {
132 return {
133 n: num(s, "n"),
134 freq: vec(s, "freq"),
135 q: vec(s, "q"),
136 zeta: vec(s, "zeta"),
137 hit: vec(s, "hit"),
138 support: vec(s, "support"),
139 radiation: vec(s, "radiation"),
140 radiation_factor: vec(s, "radiation_factor"),
141 acoustic: vec(s, "acoustic"),
142 audible: Array.from(vec(s, "audible"), (x) => x !== 0),
143 q0: vec(s, "q0"),
144 v0: vec(s, "v0"),
145 shapes: frames3d(s, "shapes"),
146 };
147}
149function parseContact(s: PlainStruct): ContactResult {
150 return {
151 model: str(s, "model"),
152 tc: num(s, "tc"),
153 impulse: num(s, "impulse"),
154 peak_force: num(s, "peak_force"),
155 t: vec(s, "t"),
156 force: vec(s, "force"),
157 hammer_x: vec(s, "hammer_x"),
158 hammer_v: vec(s, "hammer_v"),
159 plate_w: vec(s, "plate_w"),
160 plate_v: vec(s, "plate_v"),
161 hammer_exit_v: num(s, "hammer_exit_v"),
162 plate_exit_v: num(s, "plate_exit_v"),
163 relative_separation_v: num(s, "relative_separation_v"),
164 effective_restitution: num(s, "effective_restitution"),
165 hertz_duration: num(s, "hertz_duration"),
166 hertz_impulse: num(s, "hertz_impulse"),
167 hertz_peak_force: num(s, "hertz_peak_force"),
168 contact_k: num(s, "contact_k"),
169 };
170}
172/** Validate and type the strike's result struct. Throws ResultShapeError with a specific message. */
173export function parsePlateResult(v: PlainValue): PlateResult {
174 if (!isPlainStruct(v)) throw new ResultShapeError("the result variable is not a struct");
175 const grid = struct(v, "grid");
176 const probe = struct(v, "probe");
177 const frames = struct(v, "frames");
178 const env = struct(v, "envelope");
179 const spec = struct(v, "spectrum");
180 const audio = struct(v, "audio");
181 return {
182 method: methodId(str(v, "method")),
183 grid: { x: vec(grid, "x"), y: vec(grid, "y") },
184 t: vec(v, "t"),
185 probe: {
186 x: num(probe, "x"),
187 y: num(probe, "y"),
188 w: vec(probe, "w"),
189 sxx: vec(probe, "sxx"),
190 syy: vec(probe, "syy"),
191 txy: vec(probe, "txy"),
192 vm: vec(probe, "vm"),
193 sp: vec(probe, "sp"),
194 },
195 frames: {
196 t: vec(frames, "t"),
197 w: frames3d(frames, "w"),
198 w_total: frames3d(frames, "w_total"),
199 vm: frames3d(frames, "vm"),
200 sp: frames3d(frames, "sp"),
201 },
202 envelope: { vm: field2d(env, "vm"), sp: field2d(env, "sp") },
203 spectrum: { f: vec(spec, "f"), p: vec(spec, "p"), w: vec(spec, "w") },
204 audio: {
205 fs: num(audio, "fs"),
206 p: vec(audio, "p"),
207 peak_pa: num(audio, "peak_pa"),
208 gain: num(audio, "gain"),
209 normalize_dbfs: num(audio, "normalize_dbfs"),
210 model: str(audio, "model"),
211 },
212 modes: parseModes(struct(v, "modes")),
213 contact: parseContact(struct(v, "contact")),
214 info: parseInfo(struct(v, "info")),
215 };
216}
218export function parseMethodPlan(v: PlainValue): MethodPlan {
219 if (!isPlainStruct(v)) throw new ResultShapeError("the plan variable is not a struct");
220 const opt = (name: string) => (v.fields[name] === undefined ? undefined : num(v, name));
221 return {
222 method: methodId(str(v, "method")),
223 ndof: num(v, "ndof"),
224 work: num(v, "work"),
225 size_note: str(v, "size_note"),
226 plate_mass: num(v, "plate_mass"),
227 thread_k: num(v, "thread_k"),
228 pendulum_hz: num(v, "pendulum_hz"),
229 f_max: num(v, "f_max"),
230 modes_expected: num(v, "modes_expected"),
231 modes_cap: num(v, "modes_cap"),
232 hertz_duration: num(v, "hertz_duration"),
233 hertz_impulse: num(v, "hertz_impulse"),
234 hertz_peak_force: num(v, "hertz_peak_force"),
235 contact_k: num(v, "contact_k"),
236 critical_frequency: num(v, "critical_frequency"),
237 nsamples: num(v, "nsamples"),
238 nframes: num(v, "nframes"),
239 notes: strings(v, "notes"),
240 nx: opt("nx"),
241 ny: opt("ny"),
242 dx: opt("dx"),
243 dy: opt("dy"),
244 };
245}
247/** Sanity check of a modes struct read back from stage 1, before it is kept. */
248export function checkModesValue(v: PlainValue): { method: MethodId; nmodes: number } {
249 if (!isPlainStruct(v)) throw new ResultShapeError("the modes variable is not a struct");
250 const method = methodId(str(v, "method"));
251 const freqs = vec(v, "freqs");
252 return { method, nmodes: freqs.length };
253}