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 {
10 MethodPlan,
11 PlateResult,
12 ResultInfo,
13 RigidResult,
14 StaticResult,
15 SupportsResult,
16} from "./types";
18export class ResultShapeError extends Error {}
20function field(s: PlainStruct, name: string): PlainValue {
21 const v = s.fields[name];
22 if (v === undefined) throw new ResultShapeError(`result is missing field "${name}"`);
23 return v;
24}
26function struct(s: PlainStruct, name: string): PlainStruct {
27 const v = field(s, name);
28 if (!isPlainStruct(v)) throw new ResultShapeError(`field "${name}" should be a struct`);
29 return v;
30}
32function num(s: PlainStruct, name: string): number {
33 const v = field(s, name);
34 if (typeof v === "number") return v;
35 if (typeof v === "boolean") return v ? 1 : 0;
36 if (isPlainTensor(v) && v.data.length === 1) return v.data[0];
37 throw new ResultShapeError(`field "${name}" should be a scalar`);
38}
40function bool(s: PlainStruct, name: string): boolean {
41 return num(s, name) !== 0;
42}
44function str(s: PlainStruct, name: string): string {
45 const v = field(s, name);
46 if (typeof v === "string") return v;
47 throw new ResultShapeError(`field "${name}" should be a string`);
48}
50function vec(s: PlainStruct, name: string): Float64Array {
51 const v = field(s, name);
52 if (typeof v === "number") return Float64Array.of(v);
53 if (isPlainTensor(v)) return v.data;
54 throw new ResultShapeError(`field "${name}" should be a numeric vector`);
55}
57/** A numeric vector that may be absent or empty. */
58function vecOpt(s: PlainStruct, name: string): Float64Array {
59 const v = s.fields[name];
60 if (v === undefined) return new Float64Array(0);
61 if (typeof v === "number") return Float64Array.of(v);
62 if (isPlainTensor(v)) return v.data;
63 return new Float64Array(0);
64}
66function field2d(s: PlainStruct, name: string): Field2D {
67 const v = field(s, name);
68 if (!isPlainTensor(v) || v.shape.length !== 2)
69 throw new ResultShapeError(`field "${name}" should be a 2-D array`);
70 return { ny: v.shape[0], nx: v.shape[1], data: v.data };
71}
73function field2dOpt(s: PlainStruct, name: string): Field2D | undefined {
74 const v = s.fields[name];
75 if (!isPlainTensor(v) || v.shape.length !== 2) return undefined;
76 return { ny: v.shape[0], nx: v.shape[1], data: v.data };
77}
79function frames3d(s: PlainStruct, name: string): FieldFrames {
80 const v = field(s, name);
81 if (!isPlainTensor(v)) throw new ResultShapeError(`field "${name}" should be a 3-D array`);
82 const [ny, nx, nf] = [v.shape[0] ?? 0, v.shape[1] ?? 0, v.shape[2] ?? 1];
83 if (v.shape.length < 2 || v.shape.length > 3)
84 throw new ResultShapeError(`field "${name}" should be a 3-D array`);
85 return { ny, nx, nf, data: v.data };
86}
88/** Rows of an m x n column-major matrix as m separate arrays. */
89function rows(s: PlainStruct, name: string, m: number): Float64Array[] {
90 const v = s.fields[name];
91 if (m === 0 || v === undefined || !isPlainTensor(v)) return [];
92 const [mm, n] = [v.shape[0] ?? 0, v.shape[1] ?? 0];
93 if (mm !== m) throw new ResultShapeError(`field "${name}" should have ${m} rows`);
94 const out: Float64Array[] = [];
95 for (let k = 0; k < m; k++) {
96 const r = new Float64Array(n);
97 for (let j = 0; j < n; j++) r[j] = v.data[k + j * m];
98 out.push(r);
99 }
100 return out;
101}
103function strings(s: PlainStruct, name: string): string[] {
104 const v = s.fields[name];
105 if (v === undefined) return [];
106 if (typeof v === "string") return [v];
107 if (isPlainCell(v)) return v.items.map((x) => (typeof x === "string" ? x : JSON.stringify(x)));
108 return [];
109}
111const INFO_STANDARD = new Set([
112 "nsteps",
113 "ncells",
114 "dt",
115 "fs",
116 "wall_s",
117 "stable",
118 "diverged_step",
119 "notes",
120]);
122function parseInfo(s: PlainStruct): ResultInfo {
123 const extra: Record<string, number | string | boolean> = {};
124 for (const [k, v] of Object.entries(s.fields)) {
125 if (INFO_STANDARD.has(k)) continue;
126 if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") extra[k] = v;
127 else if (isPlainTensor(v) && v.data.length === 1) extra[k] = v.data[0];
128 }
129 return {
130 nsteps: num(s, "nsteps"),
131 ncells: num(s, "ncells"),
132 dt: num(s, "dt"),
133 fs: num(s, "fs"),
134 wall_s: num(s, "wall_s"),
135 stable: bool(s, "stable"),
136 diverged_step: num(s, "diverged_step"),
137 notes: strings(s, "notes"),
138 extra,
139 };
140}
142function parseRigid(s: PlainStruct, framesStruct: PlainStruct): RigidResult {
143 const nr = num(s, "nr");
144 const ed = vec(s, "ed");
145 const eh = vec(s, "eh");
146 return {
147 nr,
148 names: strings(s, "names"),
149 series: rows(s, "series", nr),
150 frames: rows(framesStruct, "rigid", nr),
151 pivot: { x: num(s, "pivot_x"), y: num(s, "pivot_y") },
152 ed: [ed[0] ?? 1, ed[1] ?? 0],
153 eh: [eh[0] ?? 0, eh[1] ?? 1],
154 hz: Array.from(vecOpt(s, "hz")),
155 modeNames: strings(s, "mode_names"),
156 };
157}
159function parseSupports(s: PlainStruct): SupportsResult {
160 const n = num(s, "n");
161 return {
162 names: strings(s, "names"),
163 units: strings(s, "units"),
164 kinds: strings(s, "kinds"),
165 series: rows(s, "series", n),
166 static: Array.from(vecOpt(s, "static")).slice(0, n),
167 };
168}
170function parseStatic(s: PlainStruct): StaticResult {
171 return {
172 w0: field2d(s, "w0"),
173 sag: num(s, "sag"),
174 gx: num(s, "gx"),
175 gy: num(s, "gy"),
176 gz: num(s, "gz"),
177 g: num(s, "g"),
178 mass: num(s, "mass"),
179 xc: num(s, "xc"),
180 yc: num(s, "yc"),
181 held: field2d(s, "held"),
182 Nxx: field2dOpt(s, "Nxx"),
183 Nyy: field2dOpt(s, "Nyy"),
184 Nxy: field2dOpt(s, "Nxy"),
185 };
186}
188/** Validate and type the solver's result struct. Throws ResultShapeError with a specific message. */
189export function parsePlateResult(v: PlainValue): PlateResult {
190 if (!isPlainStruct(v)) throw new ResultShapeError("the result variable is not a struct");
191 const grid = struct(v, "grid");
192 const probe = struct(v, "probe");
193 const frames = struct(v, "frames");
194 const env = struct(v, "envelope");
195 const spec = struct(v, "spectrum");
196 const audio = struct(v, "audio");
197 return {
198 method: str(v, "method"),
199 grid: { x: vec(grid, "x"), y: vec(grid, "y") },
200 t: vec(v, "t"),
201 probe: {
202 x: num(probe, "x"),
203 y: num(probe, "y"),
204 w: vec(probe, "w"),
205 sxx: vec(probe, "sxx"),
206 syy: vec(probe, "syy"),
207 txy: vec(probe, "txy"),
208 vm: vec(probe, "vm"),
209 sp: vec(probe, "sp"),
210 },
211 frames: {
212 t: vec(frames, "t"),
213 w: frames3d(frames, "w"),
214 w_total: frames3d(frames, "w_total"),
215 vm: frames3d(frames, "vm"),
216 sp: frames3d(frames, "sp"),
217 },
218 envelope: { vm: field2d(env, "vm"), sp: field2d(env, "sp") },
219 spectrum: { f: vec(spec, "f"), w: vec(spec, "w"), vm: vec(spec, "vm") },
220 audio: {
221 fs: num(audio, "fs"),
222 p: vec(audio, "p"),
223 t0: num(audio, "t0"),
224 model: str(audio, "model"),
225 },
226 rigid: parseRigid(struct(v, "rigid"), frames),
227 supports: parseSupports(struct(v, "supports")),
228 static: parseStatic(struct(v, "static")),
229 info: parseInfo(struct(v, "info")),
230 };
231}
233export function parseMethodPlan(v: PlainValue): MethodPlan {
234 if (!isPlainStruct(v)) throw new ResultShapeError("the plan variable is not a struct");
235 return {
236 nx: num(v, "nx"),
237 ny: num(v, "ny"),
238 dx: num(v, "dx"),
239 dy: num(v, "dy"),
240 dt_max: num(v, "dt_max"),
241 dt: num(v, "dt"),
242 nsteps: num(v, "nsteps"),
243 ncells: num(v, "ncells"),
244 nunknowns: num(v, "nunknowns"),
245 cell_steps: num(v, "cell_steps"),
246 rigid_hz: Array.from(vecOpt(v, "rigid_hz")),
247 rigid_names: strings(v, "rigid_names"),
248 rigid_mode_names: strings(v, "rigid_mode_names"),
249 sag: num(v, "sag"),
250 gx: num(v, "gx"),
251 gy: num(v, "gy"),
252 gz: num(v, "gz"),
253 mass: num(v, "mass"),
254 xc: num(v, "xc"),
255 yc: num(v, "yc"),
256 tension: Array.from(vecOpt(v, "tension")),
257 notes: strings(v, "notes"),
258 };
259}