b1dbd02Switch sampling to pure-WASI web workers; add results dashboardJeremy Magland 1import type { WorkspaceFileSystem } from 'minwebide';
3// Loads a completed run from its output directory (the files written by
4// outputs.ts) into the shape the results dashboard works with. The run.json
5// manifest is written last, so its presence means the other files are
6// complete.
8/** The run.json manifest (the sampling configuration plus format tag). */
9export interface RunInfo {
10 format?: string;
11 stan?: string;
12 data?: string;
13 num_chains?: number;
14 num_warmup?: number;
15 num_samples?: number;
16 init_radius?: number;
17 seed?: number;
18 compute_time_sec?: number;
19}
21export interface RunVariable {
22 /** Prettified name: 'beta.1' → 'beta[1]'. */
23 name: string;
24 /** Sampler diagnostics (lp__, divergent__, ...). */
25 isDiagnostic: boolean;
26 /** draws[chain][draw]. */
27 draws: number[][];
28}
30export interface RunData {
31 info: RunInfo;
32 /** Model parameters first, diagnostics last. */
33 variables: RunVariable[];
34 numChains: number;
35 drawsPerChain: number;
36 /** Parsed summary.csv: header row + data rows. */
37 summary: string[][];
38 consoleText: string;
39}
41export async function loadRunData(fs: WorkspaceFileSystem, outputDir: string): Promise<RunData> {
42 const info = JSON.parse(await readText(fs, `${outputDir}/run.json`)) as RunInfo;
43 const numChains = info.num_chains ?? 1;
45 let paramNames: string[] = [];
46 const perChain: number[][][] = []; // [chain][param][draw]
47 for (let chain = 1; chain <= numChains; chain++) {
48 const text = await readText(fs, `${outputDir}/chain_${chain}.csv`);
49 const lines = text.split('\n').filter((line) => line.length > 0);
50 if (lines.length < 2) {
51 throw new Error(`chain_${chain}.csv has no draws`);
52 }
53 const names = lines[0].split(',');
54 if (chain === 1) {
55 paramNames = names;
56 } else if (names.length !== paramNames.length) {
57 throw new Error(`chain_${chain}.csv has a different parameter set than chain_1.csv`);
58 }
59 const draws: number[][] = names.map(() => new Array<number>(lines.length - 1));
60 for (let row = 1; row < lines.length; row++) {
61 const values = lines[row].split(',');
62 for (let p = 0; p < names.length; p++) {
63 draws[p][row - 1] = Number(values[p]);
64 }
65 }
66 perChain.push(draws);
67 }
69 const variables: RunVariable[] = paramNames.map((rawName, p) => ({
70 name: prettifyParamName(rawName),
71 isDiagnostic: rawName.endsWith('__'),
72 draws: perChain.map((chain) => chain[p]),
73 }));
74 // model parameters first, sampler diagnostics last (stable within groups)
75 variables.sort((a, b) => Number(a.isDiagnostic) - Number(b.isDiagnostic));
77 const summaryText = await readText(fs, `${outputDir}/summary.csv`).catch(() => '');
78 const summary = summaryText.split('\n').filter((line) => line.length > 0).map((line) => line.split(','));
80 const consoleText = await readText(fs, `${outputDir}/console.txt`).catch(() => '');
82 return {
83 info,
84 variables,
85 numChains,
86 drawsPerChain: variables[0]?.draws[0]?.length ?? 0,
87 summary,
88 consoleText,
89 };
90}
92/** TinyStan flattens indices with dots: 'beta.1.2' → 'beta[1,2]'. */
93export function prettifyParamName(name: string): string {
94 const [base, ...indices] = name.split('.');
95 return indices.length > 0 ? `${base}[${indices.join(',')}]` : name;
96}
98async function readText(fs: WorkspaceFileSystem, path: string): Promise<string> {
99 return (await fs.fileService.readFile(fs.root.with({ path }))).value.toString();
100}