1import { isPlainCell, isPlainStruct, isPlainTensor, type PlainStruct, type PlainValue } from "../runner/plain";
2import type { Field2D, FieldFrames } from "../fields/field";
3import type { CompareResult, MethodPlan, ModeTable, PlateResult, ResultInfo } from "./types";
5export class ResultShapeError extends Error {}
7function field(s: PlainStruct, name: string): PlainValue {
8 const v = s.fields[name];
9 if (v === undefined) throw new ResultShapeError(`result is missing field "${name}"`);
10 return v;
11}
13function struct(s: PlainStruct, name: string): PlainStruct {
14 const v = field(s, name);
15 if (!isPlainStruct(v)) throw new ResultShapeError(`field "${name}" should be a struct`);
16 return v;
17}
19function num(s: PlainStruct, name: string): number {
20 const v = field(s, name);
21 if (typeof v === "number") return v;
22 if (typeof v === "boolean") return v ? 1 : 0;
23 if (isPlainTensor(v) && v.data.length === 1) return v.data[0];
24 throw new ResultShapeError(`field "${name}" should be a scalar`);
25}
27function bool(s: PlainStruct, name: string): boolean {
28 return num(s, name) !== 0;
29}
31function str(s: PlainStruct, name: string): string {
32 const v = field(s, name);
33 if (typeof v === "string") return v;
34 throw new ResultShapeError(`field "${name}" should be a string`);
35}
37function vec(s: PlainStruct, name: string): Float64Array {
38 const v = field(s, name);
39 if (typeof v === "number") return Float64Array.of(v);
40 if (isPlainTensor(v)) return v.data;
41 throw new ResultShapeError(`field "${name}" should be a numeric vector`);
42}
44function field2d(s: PlainStruct, name: string): Field2D {
45 const v = field(s, name);
46 if (!isPlainTensor(v) || v.shape.length !== 2) throw new ResultShapeError(`field "${name}" should be a 2-D array`);
47 return { ny: v.shape[0], nx: v.shape[1], data: v.data };
48}
50function frames3d(s: PlainStruct, name: string): FieldFrames {
51 const v = field(s, name);
52 if (!isPlainTensor(v)) throw new ResultShapeError(`field "${name}" should be a 3-D array`);
53 const [ny, nx, nf] = [v.shape[0] ?? 0, v.shape[1] ?? 0, v.shape[2] ?? 1];
54 if (v.shape.length < 2 || v.shape.length > 3) throw new ResultShapeError(`field "${name}" should be a 3-D array`);
55 return { ny, nx, nf, data: v.data };
56}
58function strings(s: PlainStruct, name: string): string[] {
59 const v = s.fields[name];
60 if (v === undefined) return [];
61 if (typeof v === "string") return [v];
62 if (isPlainCell(v)) return v.items.map((x) => (typeof x === "string" ? x : JSON.stringify(x)));
63 return [];
64}
66const INFO_STANDARD = new Set(["nsteps", "ncells", "dt", "fs", "wall_s", "stable", "diverged_step", "notes"]);
68function parseInfo(s: PlainStruct): ResultInfo {
69 const extra: Record<string, number | string | boolean> = {};
70 for (const [k, v] of Object.entries(s.fields)) {
71 if (INFO_STANDARD.has(k)) continue;
72 if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") extra[k] = v;
73 else if (isPlainTensor(v) && v.data.length === 1) extra[k] = v.data[0];
74 }
75 return {
76 nsteps: num(s, "nsteps"),
77 ncells: num(s, "ncells"),
78 dt: num(s, "dt"),
79 fs: num(s, "fs"),
80 wall_s: num(s, "wall_s"),
81 stable: bool(s, "stable"),
82 diverged_step: num(s, "diverged_step"),
83 notes: strings(s, "notes"),
84 extra,
85 };
86}
88function parseModes(s: PlainStruct): ModeTable {
89 return {
90 m: vec(s, "m"),
91 n: vec(s, "n"),
92 f_hz: vec(s, "f_hz"),
93 sigma: vec(s, "sigma"),
94 G: vec(s, "G"),
95 truncated: bool(s, "truncated"),
96 };
97}
99/** Validate and type the solver's result struct. Throws ResultShapeError with a specific message. */
100export function parsePlateResult(v: PlainValue): PlateResult {
101 if (!isPlainStruct(v)) throw new ResultShapeError("the result variable is not a struct");
102 const grid = struct(v, "grid");
103 const probe = struct(v, "probe");
104 const frames = struct(v, "frames");
105 const env = struct(v, "envelope");
106 const spec = struct(v, "spectrum");
107 const audio = struct(v, "audio");
108 const out: PlateResult = {
109 method: str(v, "method"),
110 grid: { x: vec(grid, "x"), y: vec(grid, "y") },
111 t: vec(v, "t"),
112 probe: {
113 x: num(probe, "x"),
114 y: num(probe, "y"),
115 w: vec(probe, "w"),
116 sxx: vec(probe, "sxx"),
117 syy: vec(probe, "syy"),
118 txy: vec(probe, "txy"),
119 vm: vec(probe, "vm"),
120 sp: vec(probe, "sp"),
121 },
122 frames: { t: vec(frames, "t"), w: frames3d(frames, "w"), vm: frames3d(frames, "vm"), sp: frames3d(frames, "sp") },
123 envelope: { vm: field2d(env, "vm"), sp: field2d(env, "sp") },
124 spectrum: { f: vec(spec, "f"), w: vec(spec, "w"), vm: vec(spec, "vm") },
125 audio: { fs: num(audio, "fs"), p: vec(audio, "p"), t0: num(audio, "t0") },
126 info: parseInfo(struct(v, "info")),
127 };
128 if (isPlainStruct(v.fields.modes)) out.modes = parseModes(v.fields.modes);
129 return out;
130}
132export function parseMethodPlan(v: PlainValue): MethodPlan {
133 if (!isPlainStruct(v)) throw new ResultShapeError("the plan variable is not a struct");
134 return {
135 nx: num(v, "nx"),
136 ny: num(v, "ny"),
137 dx: num(v, "dx"),
138 dy: num(v, "dy"),
139 dt_max: v.fields.dt_max !== undefined ? num(v, "dt_max") : undefined,
140 dt: num(v, "dt"),
141 nsteps: num(v, "nsteps"),
142 ncells: num(v, "ncells"),
143 cell_steps: num(v, "cell_steps"),
144 };
145}
147export function parseCompareResult(v: PlainValue): CompareResult {
148 if (!isPlainStruct(v)) throw new ResultShapeError("the comparison variable is not a struct");
149 const fd = v.fields.frames_diff;
150 const framesDiff = isPlainTensor(fd) && fd.data.length > 0 ? frames3d(v, "frames_diff") : null;
151 return {
152 t: vec(v, "t"),
153 w_a: vec(v, "w_a"),
154 w_b: vec(v, "w_b"),
155 vm_a: vec(v, "vm_a"),
156 vm_b: vec(v, "vm_b"),
157 w_rel_l2: num(v, "w_rel_l2"),
158 vm_rel_l2: num(v, "vm_rel_l2"),
159 w_peak_a: num(v, "w_peak_a"),
160 w_peak_b: num(v, "w_peak_b"),
161 vm_peak_a: num(v, "vm_peak_a"),
162 vm_peak_b: num(v, "vm_peak_b"),
163 f_peak_a: num(v, "f_peak_a"),
164 f_peak_b: num(v, "f_peak_b"),
165 frames_t: vec(v, "frames_t"),
166 frames_rel_l2: vec(v, "frames_rel_l2"),
167 frames_diff: framesDiff,
168 };
169}
171/** @deprecated use parseMethodPlan */
172export const parseFdPlan = parseMethodPlan;