@barivia/barmesh-mcp 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,7 +92,7 @@ export function formatSnapshotLine(s) {
92
92
  if (s.qe != null)
93
93
  parts.push(`QE ${s.qe.toFixed(4)}`);
94
94
  if (s.te != null)
95
- parts.push(`TE ${s.te.toFixed(4)}`);
95
+ parts.push(`Epoch TE ${s.te.toFixed(4)}`);
96
96
  if (s.eta_sec != null)
97
97
  parts.push(`ETA ~${s.eta_sec}s`);
98
98
  if (s.ordering_errors_tail?.length) {
@@ -3,7 +3,7 @@
3
3
  * Surfaces phase, elapsed, ETA, and epoch N/M during the SOM training of a
4
4
  * cfd_mesh_convergence job, plus CFD finalize (figure render) sub-status.
5
5
  */
6
- import { apiCall } from "./shared.js";
6
+ import { formatRunConfigTable, finalizeSubStatusLine } from "./run_config.js";
7
7
  function formatElapsedPart(data, status) {
8
8
  const wall = data.wall_elapsed_sec != null && !Number.isNaN(Number(data.wall_elapsed_sec))
9
9
  ? Number(data.wall_elapsed_sec)
@@ -33,32 +33,16 @@ function formatElapsedPart(data, status) {
33
33
  }
34
34
  return null;
35
35
  }
36
- async function finalizeStatusLine(finalizeId) {
37
- try {
38
- const fin = (await apiCall("GET", `/v1/jobs/${finalizeId}`));
39
- const st = String(fin.status ?? "unknown");
40
- const prog = fin.progress != null ? `${(Number(fin.progress) * 100).toFixed(0)}%` : null;
41
- const phase = fin.progress_phase != null ? String(fin.progress_phase) : null;
42
- const elapsed = formatElapsedPart(fin, st);
43
- const bits = [`cfd_finalize ${finalizeId}: ${st}`];
44
- if (prog)
45
- bits.push(prog);
46
- if (phase)
47
- bits.push(`phase ${phase}`);
48
- if (elapsed)
49
- bits.push(elapsed);
50
- return bits.join(", ");
51
- }
52
- catch {
53
- return `cfd_finalize ${finalizeId} still running (poll barmesh_jobs(action=status, job_id="${finalizeId}"))`;
54
- }
55
- }
56
36
  export async function formatJobStatusText(job_id, data) {
57
37
  const status = String(data.status ?? "unknown");
58
38
  const progress = (data.progress ?? 0) * 100;
59
39
  const label = data.label != null && data.label !== "" ? String(data.label) : null;
60
40
  const jobDesc = label ? `Job ${label} (id: ${job_id})` : `Job ${job_id}`;
61
- const parts = [`${jobDesc}: ${status} (${progress.toFixed(1)}%)`];
41
+ const parts = [];
42
+ const runConfig = formatRunConfigTable(data);
43
+ if (runConfig)
44
+ parts.push(runConfig);
45
+ parts.push(`${jobDesc}: ${status} (${progress.toFixed(1)}%)`);
62
46
  const attempt = data.attempt != null ? Number(data.attempt) : null;
63
47
  if (attempt != null && attempt > 1) {
64
48
  parts.push(`attempt ${attempt} (job was requeued after worker timeout — not stuck from scratch)`);
@@ -86,14 +70,19 @@ export async function formatJobStatusText(job_id, data) {
86
70
  const qe = data.quantization_error != null ? Number(data.quantization_error) : null;
87
71
  if (qe != null && !Number.isNaN(qe))
88
72
  parts.push(`QE ${qe.toFixed(4)}`);
73
+ const mapTe = data.map_topographic_error != null ? Number(data.map_topographic_error) : null;
74
+ const liveTe = data.topographic_error != null ? Number(data.topographic_error) : null;
75
+ const te = mapTe != null && !Number.isNaN(mapTe) ? mapTe : liveTe;
76
+ if (te != null && !Number.isNaN(te))
77
+ parts.push(`TE ${te.toFixed(4)}`);
89
78
  }
90
79
  if (status === "running" && progress < 5 && !phase) {
91
80
  parts.push("Advisory: still in early preprocessing — epoch counter starts after ingest completes.");
92
81
  }
93
82
  if (status === "completed") {
94
- const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== "" ? String(data.finalize_job_id) : null;
95
- if (finalizeId) {
96
- parts.push(await finalizeStatusLine(finalizeId));
83
+ const finalizeLine = finalizeSubStatusLine(data);
84
+ if (finalizeLine) {
85
+ parts.push(finalizeLine);
97
86
  parts.push(`Compute done; figures may still be uploading. Poll barmesh_jobs(status) until finalize completes before barmesh_results(get).`);
98
87
  }
99
88
  else {
@@ -0,0 +1,136 @@
1
+ /** Run configuration summary for training monitor (mesh_convergence + training jobs). */
2
+ function formatGrid(grid) {
3
+ if (!Array.isArray(grid) || grid.length < 2)
4
+ return null;
5
+ return `${grid[0]}×${grid[1]}`;
6
+ }
7
+ function formatEpochs(epochs) {
8
+ if (!Array.isArray(epochs) || epochs.length === 0)
9
+ return null;
10
+ if (epochs.length >= 2)
11
+ return `${epochs[0]}+${epochs[1]}`;
12
+ return String(epochs[0]);
13
+ }
14
+ function formatMeshOrder(mo) {
15
+ if (!Array.isArray(mo) || mo.length === 0)
16
+ return null;
17
+ return mo.map(String).join("→");
18
+ }
19
+ function formatFeatures(feats) {
20
+ if (!Array.isArray(feats) || feats.length === 0)
21
+ return null;
22
+ const names = feats.map(String);
23
+ if (names.length <= 6)
24
+ return names.join(", ");
25
+ return `${names.length} cols: ${names.slice(0, 5).join(", ")}, …`;
26
+ }
27
+ /** Merge nested run_config from GET /v1/jobs/:id with top-level monitor fields. */
28
+ export function runConfigSource(data) {
29
+ const rc = data.run_config;
30
+ if (rc && typeof rc === "object") {
31
+ return { ...data, ...rc };
32
+ }
33
+ return data;
34
+ }
35
+ export function buildRunConfigRows(data) {
36
+ const src = runConfigSource(data);
37
+ const rows = [];
38
+ if (src.label)
39
+ rows.push(["Label", String(src.label)]);
40
+ const dsName = src.dataset_name ?? data.dataset_name;
41
+ const dsId = src.dataset_id ?? data.dataset_id;
42
+ if (dsName) {
43
+ const idHint = dsId ? ` (${String(dsId).slice(0, 8)}…)` : "";
44
+ rows.push(["Dataset", `${String(dsName)}${idHint}`]);
45
+ }
46
+ else if (dsId) {
47
+ rows.push(["Dataset id", String(dsId)]);
48
+ }
49
+ const featStr = formatFeatures(src.feature_columns ?? src.columns);
50
+ if (featStr)
51
+ rows.push(["Features", featStr]);
52
+ if (src.batch_size != null)
53
+ rows.push(["Batch", String(src.batch_size)]);
54
+ if (src.preset)
55
+ rows.push(["Preset", String(src.preset)]);
56
+ const gridStr = formatGrid(src.grid);
57
+ if (gridStr)
58
+ rows.push(["Grid", gridStr]);
59
+ const epStr = formatEpochs(src.epochs);
60
+ if (epStr)
61
+ rows.push(["Epochs", epStr]);
62
+ if (src.reference_mesh)
63
+ rows.push(["Reference", String(src.reference_mesh)]);
64
+ const moStr = formatMeshOrder(src.mesh_order);
65
+ if (moStr)
66
+ rows.push(["Mesh order", moStr]);
67
+ if (src.stratify_scale != null)
68
+ rows.push(["Stratify", String(src.stratify_scale)]);
69
+ if (src.emd_method)
70
+ rows.push(["EMD", String(src.emd_method)]);
71
+ if (src.backend)
72
+ rows.push(["Backend", String(src.backend)]);
73
+ if (data.dataset_rows != null) {
74
+ rows.push(["Rows", Number(data.dataset_rows).toLocaleString()]);
75
+ }
76
+ if (data.attempt != null && Number(data.attempt) > 1) {
77
+ rows.push(["Attempt", String(data.attempt)]);
78
+ }
79
+ return rows;
80
+ }
81
+ export function formatRunConfigTable(data) {
82
+ const rows = buildRunConfigRows(data);
83
+ if (rows.length === 0)
84
+ return "";
85
+ return `Run config — ${rows.map(([l, v]) => `${l}: ${v}`).join(" | ")}`;
86
+ }
87
+ export function finalizeSubStatusLine(data) {
88
+ const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
89
+ ? String(data.finalize_job_id)
90
+ : null;
91
+ if (!finalizeId)
92
+ return null;
93
+ const st = String(data.finalize_status ?? "unknown");
94
+ const prog = data.finalize_progress != null ? `${(Number(data.finalize_progress) * 100).toFixed(0)}%` : null;
95
+ const phase = data.finalize_phase != null && String(data.finalize_phase) !== ""
96
+ ? String(data.finalize_phase)
97
+ : null;
98
+ const bits = [`cfd_finalize ${finalizeId}: ${st}`];
99
+ if (prog)
100
+ bits.push(prog);
101
+ if (phase)
102
+ bits.push(`phase ${phase}`);
103
+ return bits.join(", ");
104
+ }
105
+ export function isTerminalJobStatus(status) {
106
+ return status === "completed" || status === "failed" || status === "cancelled";
107
+ }
108
+ /** Keep polling while finalize is still running after parent compute completed. */
109
+ export function shouldKeepPollingJob(data) {
110
+ const status = String(data.status ?? "");
111
+ if (!isTerminalJobStatus(status))
112
+ return true;
113
+ if (status !== "completed")
114
+ return false;
115
+ if (data.finalize_failed === true)
116
+ return false;
117
+ const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
118
+ ? String(data.finalize_job_id)
119
+ : null;
120
+ const finalizeStatus = data.finalize_status != null && String(data.finalize_status) !== ""
121
+ ? String(data.finalize_status)
122
+ : null;
123
+ return finalizeId != null && finalizeStatus !== "completed";
124
+ }
125
+ /** True when compute + finalize (if any) are done — safe to show results_explorer hint. */
126
+ export function isFullyComplete(data) {
127
+ const status = String(data.status ?? "");
128
+ if (status !== "completed")
129
+ return isTerminalJobStatus(status);
130
+ const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
131
+ ? String(data.finalize_job_id)
132
+ : null;
133
+ if (!finalizeId)
134
+ return true;
135
+ return String(data.finalize_status ?? "") === "completed";
136
+ }
package/dist/shared.js CHANGED
@@ -22,7 +22,7 @@ export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ??
22
22
  export const MAX_RETRIES = 2;
23
23
  export const RETRYABLE_STATUS = new Set([502, 503, 504]);
24
24
  /** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
25
- export const CLIENT_VERSION = "0.5.4";
25
+ export const CLIENT_VERSION = "0.6.2";
26
26
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
27
27
  /** Large per-cell CSV uploads may exceed the default fetch timeout. */
28
28
  export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
@@ -163,7 +163,10 @@ async function handleResultsExplorer(job_id) {
163
163
  `This localhost port is assigned per MCP session and changes if the proxy restarts — re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.`,
164
164
  });
165
165
  }
166
- return structuredTextResult(payload, undefined, content);
166
+ return {
167
+ ...structuredTextResult(payload, undefined, content),
168
+ _meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
169
+ };
167
170
  }
168
171
  export function registerResultsExplorerTool(server) {
169
172
  const toolConfig = {
package/dist/tools/cfd.js CHANGED
@@ -25,6 +25,7 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
25
25
  backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().describe("Compute backend (default auto / preset)"),
26
26
  stratify_scale: z.number().optional().describe("[0,1] per-mesh training-row cap; 1 uses all cells (default 1)"),
27
27
  emd_method: z.enum(["exact", "sinkhorn"]).optional().describe("EMD solver: exact LP (default) or sinkhorn (fast approximation for large grids)"),
28
+ te_inner_samples: z.number().int().optional().describe("Inner statistical sample count for per-batch topographic error estimates during SOM training (default clamp(grid_nodes*6, 500, 10000); display cap remains ≤1000 batch points/phase)"),
28
29
  component_planes_physical: z.boolean().optional().describe("Physical-scale component-plane colorbars (default true)"),
29
30
  figures: z.boolean().optional().describe("Generate publication figures (default true)"),
30
31
  transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "none"])).optional().describe("Per-feature transform applied before normalization (e.g. log1p to compress k/epsilon/omega). Same preprocessing engine as barsom training."),
@@ -2,7 +2,8 @@ import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
4
  import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
5
- import { enrichWithTrainingLog, hasCurveArrays } from "../training_review.js";
5
+ import { enrichWithTrainingLog, needsTrainingLogEnrichment } from "../training_review.js";
6
+ import { formatRunConfigTable } from "../run_config.js";
6
7
  export const TRAINING_MONITOR_URI = "ui://barmesh/training-monitor";
7
8
  export const TRAINING_MONITOR_REFRESH_MS = 5000;
8
9
  function buildStructuredContent(job_id, data) {
@@ -33,7 +34,7 @@ export function registerTrainingMonitorTool(server) {
33
34
  let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
34
35
  const jobStatus = String(data.status ?? "");
35
36
  const terminal = jobStatus === "completed" || jobStatus === "failed";
36
- if (fetch_training_log || (terminal && !hasCurveArrays(data))) {
37
+ if (fetch_training_log || needsTrainingLogEnrichment(data)) {
37
38
  data = await enrichWithTrainingLog(job_id, data);
38
39
  }
39
40
  const structuredContent = buildStructuredContent(job_id, data);
@@ -51,7 +52,9 @@ export function registerTrainingMonitorTool(server) {
51
52
  timingParts.push(`epoch ${epoch}/${totalEpochs}`);
52
53
  const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
53
54
  const modeNote = terminal ? " (post-hoc review — training-log curves)" : "";
54
- const text = `Training monitor (visual MCP App, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${jobStatus} (${progress.toFixed(1)}%).${modeNote}${timingNote}`;
55
+ const runConfigNote = formatRunConfigTable(data);
56
+ const text = (runConfigNote ? `${runConfigNote}\n` : "") +
57
+ `Training monitor (visual MCP App, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${jobStatus} (${progress.toFixed(1)}%).${modeNote}${timingNote}`;
55
58
  const content = [{ type: "text", text }];
56
59
  const port = getVizPort();
57
60
  const standaloneUrl = port
@@ -71,6 +74,9 @@ export function registerTrainingMonitorTool(server) {
71
74
  text: `Interactive training monitor: [Open training monitor](${standaloneUrl})`,
72
75
  });
73
76
  }
74
- return structuredTextResult(structuredContent, text, content);
77
+ return {
78
+ ...structuredTextResult(structuredContent, text, content),
79
+ _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
80
+ };
75
81
  }));
76
82
  }
@@ -9,13 +9,38 @@ export function combinedBatchAxis(nOrd, nConv) {
9
9
  return [];
10
10
  return [...batchSampleAxis(nOrd, 0), ...batchSampleAxis(nConv, nOrd)];
11
11
  }
12
+ export function isKernelTrainingComplete(data, status) {
13
+ if (status === "failed" || status === "cancelled")
14
+ return true;
15
+ if (data.kernel_complete === true)
16
+ return true;
17
+ return status === "completed";
18
+ }
19
+ /** Snap the last TE curve point to authoritative full-map TE when kernel training is done. */
20
+ export function snapTeCurvesToMapTe(data) {
21
+ const mapTe = data.map_topographic_error ??
22
+ (data.kernel_complete === true ? data.topographic_error : null);
23
+ if (mapTe == null || !Number.isFinite(Number(mapTe)))
24
+ return data;
25
+ const out = { ...data };
26
+ for (const key of ["ordering_topographic_errors", "convergence_topographic_errors"]) {
27
+ const arr = out[key];
28
+ if (Array.isArray(arr) && arr.length > 0) {
29
+ const copy = arr.slice();
30
+ copy[copy.length - 1] = Number(mapTe);
31
+ out[key] = copy;
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ export function teCurveLabel(base, kernelComplete) {
37
+ return kernelComplete ? `${base} (→ map TE)` : `${base} (sampled)`;
38
+ }
12
39
  export function formatCurveSourceNote(data) {
13
40
  const src = data.training_curve_source_batches;
14
- if (!src)
15
- return null;
16
41
  const parts = [];
17
- const ordTotal = typeof src.ordering === "number" ? src.ordering : null;
18
- const convTotal = typeof src.convergence === "number" ? src.convergence : null;
42
+ const ordTotal = src && typeof src.ordering === "number" ? src.ordering : null;
43
+ const convTotal = src && typeof src.convergence === "number" ? src.convergence : null;
19
44
  const ordShown = Array.isArray(data.ordering_errors) ? data.ordering_errors.length : 0;
20
45
  const convShown = Array.isArray(data.convergence_errors) ? data.convergence_errors.length : 0;
21
46
  if (ordTotal != null && ordTotal > ordShown) {
@@ -24,7 +49,52 @@ export function formatCurveSourceNote(data) {
24
49
  if (convTotal != null && convTotal > convShown) {
25
50
  parts.push(`${convShown} of ${convTotal.toLocaleString()} convergence batch samples`);
26
51
  }
52
+ const mapTe = data.map_topographic_error;
53
+ const epochTe = data.epoch_topographic_error;
54
+ if (mapTe != null &&
55
+ epochTe != null &&
56
+ Number.isFinite(Number(mapTe)) &&
57
+ Number.isFinite(Number(epochTe)) &&
58
+ Math.abs(Number(epochTe) - Number(mapTe)) > 0.0005) {
59
+ parts.push(`Live TE is a subsampled batch estimate during training; final point snaps to map TE ${Number(mapTe).toFixed(4)} (last sampled ${Number(epochTe).toFixed(4)})`);
60
+ }
61
+ else if (mapTe != null && Number.isFinite(Number(mapTe))) {
62
+ parts.push(`TE curve final point is full-map topographic error (${Number(mapTe).toFixed(4)}).`);
63
+ }
27
64
  if (parts.length === 0)
28
65
  return null;
29
- return `Displaying uniformly subsampled curves (≤1000 points/phase): ${parts.join("; ")}.`;
66
+ const subsample = ordTotal != null || convTotal != null
67
+ ? "Displaying uniformly subsampled QE+TE curves (≤1000 points/phase, joint indices): "
68
+ : "";
69
+ return `${subsample}${parts.join("; ")}.`.replace(/: ;/, ":");
70
+ }
71
+ /** Map TE onto QE batch axis; when batch-aligned (same length), use values directly. */
72
+ export function alignTeToQeAxis(te, qeLen) {
73
+ if (qeLen <= 0)
74
+ return [];
75
+ if (te.length === qeLen)
76
+ return te;
77
+ if (te.length === 0)
78
+ return Array(qeLen).fill(null);
79
+ if (te.length >= qeLen)
80
+ return te.slice(0, qeLen);
81
+ const pad = qeLen - te.length;
82
+ return [...Array(pad).fill(null), ...te];
83
+ }
84
+ /**
85
+ * Last sampled TE point from the (batch-aligned) live TE curve. This is the most
86
+ * recent per-epoch TE estimate, distinct from the final trained-map TE in the summary.
87
+ */
88
+ export function lastEpochTeFromCurves(data) {
89
+ const conv = data.convergence_topographic_errors;
90
+ if (Array.isArray(conv) && conv.length > 0) {
91
+ const v = Number(conv[conv.length - 1]);
92
+ return Number.isFinite(v) ? v : null;
93
+ }
94
+ const ord = data.ordering_topographic_errors;
95
+ if (Array.isArray(ord) && ord.length > 0) {
96
+ const v = Number(ord[ord.length - 1]);
97
+ return Number.isFinite(v) ? v : null;
98
+ }
99
+ return null;
30
100
  }
@@ -5,36 +5,68 @@
5
5
  import { apiCall } from "./shared.js";
6
6
  import { formatSnapshotLine, snapshotFromJob, } from "./job_monitor.js";
7
7
  import { formatJobStatusText } from "./job_status_format.js";
8
+ import { lastEpochTeFromCurves, snapTeCurvesToMapTe } from "./training_monitor_curve.js";
8
9
  export const REVIEW_MAX_SNAPSHOTS = 16;
9
10
  function isTerminalStatus(status) {
10
11
  return status === "completed" || status === "failed" || status === "cancelled";
11
12
  }
13
+ export function hasTeCurveArrays(data) {
14
+ const ord = data.ordering_topographic_errors;
15
+ const conv = data.convergence_topographic_errors;
16
+ return ((Array.isArray(ord) && ord.length > 0) || (Array.isArray(conv) && conv.length > 0));
17
+ }
12
18
  function hasCurveArrays(data) {
13
19
  const ord = data.ordering_errors;
14
20
  const conv = data.convergence_errors;
15
- return ((Array.isArray(ord) && ord.length > 0) || (Array.isArray(conv) && conv.length > 0));
21
+ return ((Array.isArray(ord) && ord.length > 0) ||
22
+ (Array.isArray(conv) && conv.length > 0) ||
23
+ hasTeCurveArrays(data));
24
+ }
25
+ /** True when a terminal job still needs training-log curves or the final map TE. */
26
+ export function needsTrainingLogEnrichment(data) {
27
+ const status = String(data.status ?? "");
28
+ if (!isTerminalStatus(status))
29
+ return false;
30
+ if (!hasCurveArrays(data))
31
+ return true;
32
+ if (!hasTeCurveArrays(data))
33
+ return true;
34
+ if (data.map_topographic_error == null && data.topographic_error == null)
35
+ return true;
36
+ return false;
16
37
  }
17
38
  export async function enrichWithTrainingLog(job_id, data) {
18
- if (hasCurveArrays(data))
19
- return data;
39
+ const status = String(data.status ?? "");
40
+ const epochTe = data.epoch_topographic_error ??
41
+ data.topographic_error ??
42
+ lastEpochTeFromCurves(data);
20
43
  try {
21
44
  const log = (await apiCall("GET", `/v1/results/${job_id}/training-log`));
22
- return {
45
+ const merged = {
23
46
  ...data,
24
47
  ordering_errors: log.ordering_errors ?? data.ordering_errors,
25
48
  convergence_errors: log.convergence_errors ?? data.convergence_errors,
26
49
  ordering_topographic_errors: log.ordering_topographic_errors ?? data.ordering_topographic_errors,
27
50
  convergence_topographic_errors: log.convergence_topographic_errors ?? data.convergence_topographic_errors,
28
51
  quantization_error: log.quantization_error ?? data.quantization_error,
29
- topographic_error: log.topographic_error ?? data.topographic_error,
30
52
  grid: log.grid ?? data.grid,
31
53
  epochs: log.epochs ?? data.epochs,
32
54
  training_duration_seconds: log.training_duration_seconds ?? data.training_duration_seconds,
33
55
  training_curve_source_batches: log.training_curve_source_batches ?? data.training_curve_source_batches,
34
56
  };
57
+ const mapTe = log.topographic_error ?? data.map_topographic_error ?? data.topographic_error;
58
+ merged.epoch_topographic_error = epochTe ?? lastEpochTeFromCurves(merged);
59
+ merged.map_topographic_error = mapTe;
60
+ if (isTerminalStatus(status)) {
61
+ merged.topographic_error = mapTe;
62
+ }
63
+ return snapTeCurvesToMapTe(merged);
35
64
  }
36
65
  catch {
37
- return data;
66
+ if (epochTe != null) {
67
+ return snapTeCurvesToMapTe({ ...data, epoch_topographic_error: epochTe });
68
+ }
69
+ return data.kernel_complete === true ? snapTeCurvesToMapTe(data) : data;
38
70
  }
39
71
  }
40
72
  /** Evenly sample indices for a compact epoch/QE timeline in review mode. */
@@ -134,11 +166,19 @@ export function formatReviewMonitorText(result, review) {
134
166
  if (review.timing)
135
167
  lines.push(`Timing: ${review.timing}`);
136
168
  const qe = result.data.quantization_error;
137
- const te = result.data.topographic_error;
138
- if (qe != null || te != null) {
169
+ const mapTe = result.data.map_topographic_error ?? result.data.topographic_error;
170
+ const epochTe = result.data.epoch_topographic_error;
171
+ if (qe != null || mapTe != null || epochTe != null) {
139
172
  const qeS = qe != null ? `QE ${Number(qe).toFixed(4)}` : "";
140
- const teS = te != null ? `TE ${Number(te).toFixed(4)}` : "";
141
- lines.push(`Final: ${[qeS, teS].filter(Boolean).join(", ")}`);
173
+ const mapS = mapTe != null ? `Map TE ${Number(mapTe).toFixed(4)}` : "";
174
+ const epochS = epochTe != null &&
175
+ mapTe != null &&
176
+ Math.abs(Number(epochTe) - Number(mapTe)) > 0.0005
177
+ ? `Last epoch TE ${Number(epochTe).toFixed(4)}`
178
+ : epochTe != null && mapTe == null
179
+ ? `Epoch TE ${Number(epochTe).toFixed(4)}`
180
+ : "";
181
+ lines.push(`Final: ${[qeS, mapS, epochS].filter(Boolean).join(", ")}`);
142
182
  }
143
183
  lines.push("");
144
184
  lines.push("Training curve (sampled from ordering_errors / convergence_errors):");