WorkbenchRunExporter.java
package org.hammer.audio.experimental.acoustic.workbench;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.hammer.audio.experimental.acoustic.benchmark.BenchmarkReport;
import org.hammer.audio.experimental.acoustic.tracking.FrequencyCluster;
import org.hammer.audio.experimental.acoustic.tracking.TrackedSource;
import org.hammer.audio.experimental.acoustic.tracking.TrackingSnapshot;
/**
* Exports a {@link WorkbenchRunResult} to Markdown, CSV or JSON text.
*
* <p>All methods are pure functions that return a {@link String}; no I/O is performed here. The
* caller is responsible for writing the result to disk or a UI component.
*/
public final class WorkbenchRunExporter {
private static final String FMT_4F = "%.4f";
private static final String FMT_6F = "%.6f";
private static final String FMT_MICROSECONDS_2F = "%.2f µs";
private static final String MARKDOWN_PIPE_SEPARATOR = " | ";
private static final String PARAM_RESULT = "result";
private static final String NA = "n/a";
private WorkbenchRunExporter() {
// utility class
}
/**
* Produce a Markdown benchmark summary for the run result.
*
* <p>The summary is derived from the {@link BenchmarkReport} that was computed by comparing the
* estimated tracking output against the scenario ground truth. If no benchmark report is
* available (e.g. the run produced no snapshots or benchmark computation failed), a short
* placeholder message is returned. Localization and frequency metric values that could not be
* computed (evaluatedCount == 0) are rendered as {@code n/a}.
*
* @param result the run result to format
* @return a Markdown-formatted benchmark summary string
*/
public static String toBenchmarkMarkdown(WorkbenchRunResult result) {
Objects.requireNonNull(result, PARAM_RESULT);
BenchmarkReport report = result.benchmarkReport();
if (report == null) {
return "# Benchmark Report\n\n"
+ "*Not available — run produced no snapshots or benchmark computation failed.*\n";
}
StringBuilder sb = new StringBuilder(512);
sb.append("# Benchmark Report — ")
.append(result.scenario().name())
.append(
"\n\n> Comparison of estimated tracking output against scenario ground truth.\n"
+ "> All metrics are experimental.\n\n")
.append(report.toMarkdownSummary())
.append("\n\n## Localization Details\n\n| Metric | Value |\n|---|---|\n");
appendRow(
sb,
"Mean position error (m)",
formatMetric(FMT_4F, report.localization().meanDistanceErrorMeters()));
appendRow(
sb,
"Median position error (m)",
formatMetric(FMT_4F, report.localization().medianDistanceErrorMeters()));
appendRow(
sb,
"Mean angular error (°)",
formatMetric(FMT_4F, report.localization().meanAngularErrorDegrees()));
appendRow(sb, "Evaluated samples", report.localization().evaluatedCount());
appendRow(sb, "Skipped samples", report.localization().skippedCount());
sb.append("\n## Frequency Details\n\n| Metric | Value |\n|---|---|\n");
appendRow(
sb,
"Mean absolute error (Hz)",
formatMetric(FMT_4F, report.frequency().meanAbsoluteErrorHz()));
appendRow(
sb,
"Median absolute error (Hz)",
formatMetric(FMT_4F, report.frequency().medianAbsoluteErrorHz()));
appendRow(
sb, "Mean relative error", formatMetric(FMT_6F, report.frequency().meanRelativeError()));
appendRow(sb, "Evaluated samples", report.frequency().evaluatedCount());
sb.append("\n## Tracking Quality\n\n| Metric | Value |\n|---|---|\n");
appendRow(sb, "Expected sources", report.expectedSourceCount());
appendRow(sb, "Snapshot count", report.snapshotCount());
appendRow(sb, "Track continuity", formatMetric(FMT_4F, report.trackContinuity()));
appendRow(sb, "ID stability", formatMetric(FMT_4F, report.idStability()));
appendRow(sb, "False-positive rate", formatMetric(FMT_4F, report.falsePositiveRate()));
appendRow(sb, "False-negative rate", formatMetric(FMT_4F, report.falseNegativeRate()));
appendRow(sb, "Mean processing (ns)", report.meanProcessingNanos());
return sb.toString();
}
/**
* Produce a Markdown summary of the run result.
*
* <p>The summary includes: scenario metadata, parameter overview, overall statistics and a
* per-block table of frame index, cluster count, track count and processing time.
*
* @param result the run result to format
* @return a Markdown-formatted summary string
*/
public static String toMarkdown(WorkbenchRunResult result) {
Objects.requireNonNull(result, PARAM_RESULT);
StringBuilder sb = new StringBuilder(2048);
sb.append(
"# Acoustic Localization Workbench — Run Summary\n\n"
+ "> **Experimental research output.** "
+ "This is not a production localization result.\n\n"
+ "## Scenario\n\n"
+ "- **Name:** ")
.append(result.scenario().name())
.append("\n- **Room:** ")
.append(
String.format(
Locale.ROOT,
"%.1f × %.1f m",
result.scenario().room().widthMeters(),
result.scenario().room().heightMeters()))
.append("\n- **Emitters:** ")
.append(result.scenario().emitters().size())
.append("\n- **Sample rate:** ")
.append(String.format(Locale.ROOT, "%.0f Hz", (double) result.scenario().sampleRate()))
.append("\n- **Duration:** ")
.append(String.format(Locale.ROOT, "%.2f s", result.scenario().durationSeconds()))
.append("\n\n## Parameters\n\n| Parameter | Value |\n|---|---|\n");
WorkbenchParameters p = result.parameters();
appendRow(sb, "Block size", p.blockSize());
appendRow(sb, "FFT size", p.fftSize());
appendRow(sb, "Max peaks", p.maxPeaks());
appendRow(sb, "Min SNR", String.format(Locale.ROOT, "%.1f", p.minSnr()));
appendRow(
sb, "Band", String.format(Locale.ROOT, "%.0f – %.0f Hz", p.bandMinHz(), p.bandMaxHz()));
appendRow(
sb,
"Clustering tolerance",
String.format(Locale.ROOT, "%.0f Hz", p.clusteringToleranceHz()));
appendRow(sb, "Grid steps", p.candidateGridSteps());
appendRow(sb, "TDOA estimator", p.tdoaEstimatorType());
appendRow(
sb,
"Tracker freq. match",
String.format(Locale.ROOT, "%.0f Hz", p.trackerFrequencyMatchHz()));
sb.append("\n## Statistics\n\n| Metric | Value |\n|---|---|\n");
appendRow(sb, "Blocks processed", result.blockCount());
appendRow(sb, "Any tracked", result.anyTracked());
appendRow(sb, "Max tracks in a frame", result.maxTracksInAnyFrame());
appendRow(sb, "Distinct track IDs", result.distinctTrackCount());
appendRow(sb, "Synchronization modes", formatSynchronizationModes(result));
appendRow(sb, "Worst synchronization", result.worstSynchronizationStatus());
appendRow(
sb,
"Mean TDOA consistency",
String.format(Locale.ROOT, FMT_4F, result.meanTdoaConsistencyScore()));
appendRow(
sb,
"Maximum TDOA cycle residual",
String.format(
Locale.ROOT,
FMT_MICROSECONDS_2F,
result.maximumTdoaCycleResidualSeconds() * 1_000_000.0));
appendRow(sb, "Physical TDOA violations", result.physicalTdoaViolationCount());
appendRow(sb, "Unreliable TDOA frames", result.unreliableTdoaFrameCount());
appendRow(
sb,
"Avg processing / block",
String.format(
Locale.ROOT, FMT_MICROSECONDS_2F, result.averageProcessingNanosPerBlock() / 1_000.0));
appendRow(
sb,
"Max processing / block",
String.format(
Locale.ROOT, FMT_MICROSECONDS_2F, result.maxProcessingNanosPerBlock() / 1_000.0));
if (result.frameSchedule() != null) {
appendRow(
sb,
"Budget per block",
String.format(
Locale.ROOT,
FMT_MICROSECONDS_2F,
result.frameSchedule().maxProcessingNanos() / 1_000.0));
appendRow(sb, "Over-budget frames", result.overBudgetFrameCount());
}
sb.append(
"\n"
+ "## Frame-by-frame summary\n\n"
+ "| Frame | Time (ms) | Clusters | Tracks | Proc. (µs) | Sync | Error (samples) |"
+ " TDOA score | Cycle residual (µs) | Physical | Budget |\n"
+ "|---|---|---|---|---|---|---|---|---|---|---|\n");
for (TrackingSnapshot snap : result.snapshots()) {
sb.append("| ")
.append(snap.sourceFrameIndex())
.append(MARKDOWN_PIPE_SEPARATOR)
.append(String.format(Locale.ROOT, "%.1f", snap.sourceTimestampNanos() / 1_000_000.0))
.append(MARKDOWN_PIPE_SEPARATOR)
.append(snap.clusters().size())
.append(MARKDOWN_PIPE_SEPARATOR)
.append(snap.tracks().size())
.append(MARKDOWN_PIPE_SEPARATOR)
.append(String.format(Locale.ROOT, "%.1f", snap.processingNanos() / 1_000.0))
.append(MARKDOWN_PIPE_SEPARATOR)
.append(snap.synchronization().status())
.append(MARKDOWN_PIPE_SEPARATOR)
.append(
String.format(Locale.ROOT, FMT_4F, snap.synchronization().estimatedErrorSamples()))
.append(MARKDOWN_PIPE_SEPARATOR)
.append(String.format(Locale.ROOT, FMT_4F, snap.tdoaConsistency().consistencyScore()))
.append(MARKDOWN_PIPE_SEPARATOR)
.append(
String.format(
Locale.ROOT,
"%.2f",
snap.tdoaConsistency().maximumAbsoluteCycleResidualSeconds() * 1_000_000.0))
.append(MARKDOWN_PIPE_SEPARATOR)
.append(snap.tdoaConsistency().physicalViolationCount())
.append(MARKDOWN_PIPE_SEPARATOR)
.append(formatBudgetStatus(result, snap))
.append(" |\n");
}
return sb.toString();
}
/**
* Produce a CSV export of every tracked source observation across all frames.
*
* <p>Each row represents one {@link TrackedSource} in one frame. The CSV uses {@code ,} as
* separator and includes a header row.
*
* @param result the run result to format
* @return a CSV-formatted string
*/
public static String toCsv(WorkbenchRunResult result) {
Objects.requireNonNull(result, PARAM_RESULT);
StringBuilder sb = new StringBuilder(1024);
sb.append(
"frameIndex,timestampNs,trackId,frequencyHz,observedFrequencyHz,"
+ "posX,posY,velX,velY,confidence,observations,processingNs,"
+ "synchronizationMode,synchronizationStatus,synchronizationErrorSamples,"
+ "calibrationCurrent,tdoaConsistencyScore,"
+ "tdoaMaximumCycleResidualSeconds,tdoaPhysicalViolationCount,"
+ "budgetExceeded\n");
for (TrackingSnapshot snap : result.snapshots()) {
for (TrackedSource track : snap.tracks()) {
sb.append(snap.sourceFrameIndex())
.append(',')
.append(snap.sourceTimestampNanos())
.append(',')
.append(track.id())
.append(',')
.append(String.format(Locale.ROOT, "%.3f", track.frequencyHz()))
.append(',')
.append(String.format(Locale.ROOT, "%.3f", track.observedFrequencyHz()))
.append(',')
.append(String.format(Locale.ROOT, FMT_4F, track.positionMeters().x()))
.append(',')
.append(String.format(Locale.ROOT, FMT_4F, track.positionMeters().y()))
.append(',')
.append(String.format(Locale.ROOT, FMT_4F, track.velocityMetersPerSecond().x()))
.append(',')
.append(String.format(Locale.ROOT, FMT_4F, track.velocityMetersPerSecond().y()))
.append(',')
.append(String.format(Locale.ROOT, FMT_4F, track.confidence()))
.append(',')
.append(track.observationCount())
.append(',')
.append(snap.processingNanos())
.append(',')
.append(snap.synchronization().mode())
.append(',')
.append(snap.synchronization().status())
.append(',')
.append(
String.format(Locale.ROOT, FMT_6F, snap.synchronization().estimatedErrorSamples()))
.append(',')
.append(snap.synchronization().calibrationCurrent())
.append(',')
.append(String.format(Locale.ROOT, FMT_6F, snap.tdoaConsistency().consistencyScore()))
.append(',')
.append(
String.format(
Locale.ROOT,
"%.9f",
snap.tdoaConsistency().maximumAbsoluteCycleResidualSeconds()))
.append(',')
.append(snap.tdoaConsistency().physicalViolationCount())
.append(',')
.append(formatBudgetExceededCsv(result, snap))
.append('\n');
}
}
return sb.toString();
}
/**
* Produce a JSON-lines export of all snapshots (one JSON object per line).
*
* <p>Each line is a self-contained JSON object describing one frame: its index, timestamp,
* detected clusters and tracked sources. No external JSON library is required; the output is
* hand-formatted.
*
* @param result the run result to format
* @return a JSON-lines formatted string
*/
public static String toJsonLines(WorkbenchRunResult result) {
Objects.requireNonNull(result, PARAM_RESULT);
StringBuilder sb = new StringBuilder(512);
for (TrackingSnapshot snap : result.snapshots()) {
sb.append("{\"frameIndex\":")
.append(snap.sourceFrameIndex())
.append(",\"timestampNs\":")
.append(snap.sourceTimestampNanos())
.append(",\"processingNs\":")
.append(snap.processingNanos())
.append(",\"synchronization\":{\"mode\":\"")
.append(snap.synchronization().mode())
.append("\",\"status\":\"")
.append(snap.synchronization().status())
.append("\",\"estimatedErrorSamples\":")
.append(
String.format(Locale.ROOT, FMT_6F, snap.synchronization().estimatedErrorSamples()))
.append(",\"calibrationCurrent\":")
.append(snap.synchronization().calibrationCurrent())
.append("},\"tdoaConsistency\":{\"score\":")
.append(String.format(Locale.ROOT, FMT_6F, snap.tdoaConsistency().consistencyScore()))
.append(",\"evaluatedCycles\":")
.append(snap.tdoaConsistency().evaluatedCycles())
.append(",\"maximumCycleResidualSeconds\":")
.append(
String.format(
Locale.ROOT,
"%.9f",
snap.tdoaConsistency().maximumAbsoluteCycleResidualSeconds()))
.append(",\"physicalViolationCount\":")
.append(snap.tdoaConsistency().physicalViolationCount())
.append("},\"budgetExceeded\":")
.append(formatBudgetExceededJson(result, snap))
.append(",\"clusters\":[");
appendClustersJson(sb, snap.clusters());
sb.append("],\"tracks\":[");
appendTracksJson(sb, snap.tracks());
sb.append("]}\n");
}
return sb.toString();
}
private static void appendClustersJson(StringBuilder sb, List<FrequencyCluster> clusters) {
for (int i = 0; i < clusters.size(); i++) {
if (i > 0) {
sb.append(',');
}
FrequencyCluster c = clusters.get(i);
sb.append(
String.format(
Locale.ROOT,
"{\"freqHz\":%.3f,\"magnitude\":%.4f,\"channels\":%d}",
c.centerFrequencyHz(),
c.totalMagnitude(),
c.channelCount()));
}
}
private static void appendTracksJson(StringBuilder sb, List<TrackedSource> tracks) {
for (int i = 0; i < tracks.size(); i++) {
if (i > 0) {
sb.append(',');
}
TrackedSource t = tracks.get(i);
sb.append(
String.format(
Locale.ROOT,
"{\"id\":%d,\"freqHz\":%.3f,\"x\":%.4f,\"y\":%.4f,"
+ "\"confidence\":%.4f,\"observations\":%d}",
t.id(),
t.frequencyHz(),
t.positionMeters().x(),
t.positionMeters().y(),
t.confidence(),
t.observationCount()));
}
}
/**
* Collect the distinct track IDs across all snapshots in order of first appearance.
*
* @param result the run result
* @return list of track IDs in first-seen order
*/
public static List<Integer> observedTrackIds(WorkbenchRunResult result) {
Objects.requireNonNull(result, PARAM_RESULT);
Set<Integer> seen = new LinkedHashSet<>();
for (TrackingSnapshot snap : result.snapshots()) {
for (TrackedSource track : snap.tracks()) {
seen.add(track.id());
}
}
return new ArrayList<>(seen);
}
private static String formatSynchronizationModes(WorkbenchRunResult result) {
return result.synchronizationModes().stream()
.sorted()
.map(Enum::name)
.collect(Collectors.joining(", "));
}
private static String formatBudgetStatus(WorkbenchRunResult result, TrackingSnapshot snap) {
if (result.frameSchedule() == null) {
return "N/A";
}
return result.isFrameOverBudget(snap) ? "⚠ OVER" : "OK";
}
private static String formatBudgetExceededCsv(WorkbenchRunResult result, TrackingSnapshot snap) {
if (result.frameSchedule() == null) {
return "";
}
return Boolean.toString(result.isFrameOverBudget(snap));
}
private static String formatBudgetExceededJson(WorkbenchRunResult result, TrackingSnapshot snap) {
if (result.frameSchedule() == null) {
return "null";
}
return Boolean.toString(result.isFrameOverBudget(snap));
}
private static String formatMetric(String fmt, Double value) {
return value == null ? NA : String.format(Locale.ROOT, fmt, value);
}
private static void appendRow(StringBuilder sb, String label, Object value) {
sb.append("| ").append(label).append(MARKDOWN_PIPE_SEPARATOR).append(value).append(" |\n");
}
}