/ concept-collection / stan-web-ide
Sign in
concept-collection / stan-web-ide
stan-web-ide / src / stan / outputs.ts
104 lines · 3.5 KBBlameHistoryRaw
1import type { WorkspaceFileSystem } from 'minwebide';
2import {
3 effective_sample_size,
4 mean,
5 percentile,
6 split_potential_scale_reduction,
7 std_deviation,
8} from 'mcmc-stats';
10// Writes a completed run into the .sample file's output directory:
11//
12// <output_dir>/chain_1.csv ... one CSV per chain, header = parameter names
13// <output_dir>/summary.csv mean, MCSE, sd, percentiles, ESS, Rhat
14// <output_dir>/console.txt sampler console output
15// <output_dir>/run.json the exact configuration used — written
16// LAST, so it doubles as the completion
17// marker and anchors the results dashboard
18//
19// (the per-chain CSV layout matches stan-playground's "download multiple
20// CSVs" export)
22export interface RunOutputs {
23 /** draws[param][draw], chains concatenated along the draw axis. */
24 draws: number[][];
25 paramNames: string[];
26 numChains: number;
27 consoleText: string;
28 samplingOpts: Record<string, unknown>;
29 computeTimeSec: number;
32export async function writeRunOutputs(fs: WorkspaceFileSystem, outputDir: string, run: RunOutputs): Promise<string[]> {
33 const written: string[] = [];
34 const write = async (name: string, contents: string) => {
35 const path = `${outputDir}/${name}`;
36 await fs.writeFile(path, contents);
37 written.push(path);
38 };
40 // clear previous results so the directory holds exactly this run
41 await fs.deleteFile(outputDir);
43 const numDraws = run.draws[0]?.length ?? 0;
44 const perChain = Math.floor(numDraws / run.numChains);
46 for (let chain = 0; chain < run.numChains; chain++) {
47 const lines = [run.paramNames.join(',')];
48 for (let draw = chain * perChain; draw < (chain + 1) * perChain; draw++) {
49 lines.push(run.draws.map(paramDraws => String(paramDraws[draw])).join(','));
50 }
51 await write(`chain_${chain + 1}.csv`, lines.join('\n') + '\n');
52 }
54 await write('summary.csv', summaryCsv(run));
55 await write('console.txt', run.consoleText);
56 await write('run.json', JSON.stringify({ format: 'stan-web-ide.run/1', ...run.samplingOpts }, null, 2) + '\n');
58 return written;
61function summaryCsv(run: RunOutputs): string {
62 const numDraws = run.draws[0]?.length ?? 0;
63 const perChain = Math.floor(numDraws / run.numChains);
65 // model parameters first, sampler diagnostics (lp__, divergent__, ...) last
66 const order = [...run.paramNames.keys()].sort((a, b) =>
67 Number(run.paramNames[a].endsWith('__')) - Number(run.paramNames[b].endsWith('__')));
69 const lines = ['parameter,mean,mcse,sd,p5,median,p95,ess,ess_per_sec,rhat'];
70 for (const index of order) {
71 const flat = run.draws[index];
72 const byChain = Array.from({ length: run.numChains }, (_, chain) =>
73 flat.slice(chain * perChain, (chain + 1) * perChain));
74 const sorted = [...flat].sort((a, b) => a - b);
76 const ess = safe(() => effective_sample_size(byChain));
77 const sd = safe(() => std_deviation(sorted));
78 const row = [
79 safe(() => mean(sorted)),
80 sd / Math.sqrt(ess),
81 sd,
82 safe(() => percentile(sorted, 0.05)),
83 safe(() => percentile(sorted, 0.5)),
84 safe(() => percentile(sorted, 0.95)),
85 ess,
86 run.computeTimeSec > 0 ? ess / run.computeTimeSec : NaN,
87 safe(() => split_potential_scale_reduction(byChain)),
88 ];
89 lines.push([run.paramNames[index], ...row.map(formatStat)].join(','));
90 }
91 return lines.join('\n') + '\n';
94function safe(compute: () => number): number {
95 try {
96 return compute();
97 } catch {
98 return NaN;
99 }
102function formatStat(value: number): string {
103 return Number.isFinite(value) ? String(Number(value.toPrecision(6))) : 'NaN';
moveopenescclose