WorkbenchRunResult.java
package org.hammer.audio.experimental.acoustic.workbench;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.hammer.audio.acquisition.LocalizationExperiment;
import org.hammer.audio.acquisition.SynchronizationMode;
import org.hammer.audio.acquisition.SynchronizationStatus;
import org.hammer.audio.experimental.acoustic.benchmark.BenchmarkReport;
import org.hammer.audio.experimental.acoustic.simulation.SimulationScenarios.SimulationScenario;
import org.hammer.audio.experimental.acoustic.tracking.FrameSchedule;
import org.hammer.audio.experimental.acoustic.tracking.TrackedSource;
import org.hammer.audio.experimental.acoustic.tracking.TrackingSnapshot;
/**
* Immutable result of a completed headless workbench scenario run.
*
* @param scenario the scenario that was executed
* @param parameters the parameters used during the run
* @param snapshots one {@link TrackingSnapshot} per processed audio block, in order
* @param totalProcessingNanos cumulative pipeline wall-clock time across all blocks
* @param benchmarkReport benchmark quality report comparing estimated tracks against ground truth,
* or {@code null} if benchmarking was not possible (e.g. empty run)
* @param frameSchedule the real-time budget schedule used during the run, or {@code null} when not
* available; used to identify over-budget frames
* @param experiment reproducibility, array, source and calibration metadata, or {@code null} for
* legacy callers
*/
public record WorkbenchRunResult(
SimulationScenario scenario,
WorkbenchParameters parameters,
List<TrackingSnapshot> snapshots,
long totalProcessingNanos,
BenchmarkReport benchmarkReport,
FrameSchedule frameSchedule,
LocalizationExperiment experiment) {
// Validates and defensively copies the snapshots list.
public WorkbenchRunResult {
Objects.requireNonNull(scenario, "scenario");
Objects.requireNonNull(parameters, "parameters");
Objects.requireNonNull(snapshots, "snapshots");
if (totalProcessingNanos < 0L) {
throw new IllegalArgumentException("totalProcessingNanos must be >= 0");
}
snapshots = List.copyOf(snapshots);
// benchmarkReport, frameSchedule and experiment may be null for compatibility.
}
/** Construct a result without experiment metadata. */
public WorkbenchRunResult(
SimulationScenario scenario,
WorkbenchParameters parameters,
List<TrackingSnapshot> snapshots,
long totalProcessingNanos,
BenchmarkReport benchmarkReport,
FrameSchedule frameSchedule) {
this(
scenario,
parameters,
snapshots,
totalProcessingNanos,
benchmarkReport,
frameSchedule,
null);
}
/** Construct a result without a frame schedule or experiment metadata. */
public WorkbenchRunResult(
SimulationScenario scenario,
WorkbenchParameters parameters,
List<TrackingSnapshot> snapshots,
long totalProcessingNanos,
BenchmarkReport benchmarkReport) {
this(scenario, parameters, snapshots, totalProcessingNanos, benchmarkReport, null, null);
}
/** Optional reproducibility and hardware metadata. */
public Optional<LocalizationExperiment> experimentMetadata() {
return Optional.ofNullable(experiment);
}
/** Number of audio blocks that were processed. */
public int blockCount() {
return snapshots.size();
}
/** Whether at least one tracked source appeared in any frame. */
public boolean anyTracked() {
return snapshots.stream().anyMatch(snapshot -> !snapshot.tracks().isEmpty());
}
/** Maximum number of tracked sources observed in any single frame. */
public int maxTracksInAnyFrame() {
return snapshots.stream().mapToInt(snapshot -> snapshot.tracks().size()).max().orElse(0);
}
/** Average pipeline processing time per block in nanoseconds, or zero for an empty run. */
public double averageProcessingNanosPerBlock() {
return snapshots.isEmpty() ? 0.0 : (double) totalProcessingNanos / snapshots.size();
}
/** Maximum pipeline processing time across all blocks in nanoseconds. */
public long maxProcessingNanosPerBlock() {
return snapshots.stream().mapToLong(TrackingSnapshot::processingNanos).max().orElse(0L);
}
/** Total number of distinct track IDs observed across all frames. */
public long distinctTrackCount() {
return snapshots.stream()
.flatMap(snapshot -> snapshot.tracks().stream())
.mapToInt(TrackedSource::id)
.distinct()
.count();
}
/** Returns every synchronization model used during the run. */
public Set<SynchronizationMode> synchronizationModes() {
Set<SynchronizationMode> modes = EnumSet.noneOf(SynchronizationMode.class);
snapshots.stream().map(snapshot -> snapshot.synchronization().mode()).forEach(modes::add);
return Set.copyOf(modes);
}
/** Returns the most severe synchronization status observed during the run. */
public SynchronizationStatus worstSynchronizationStatus() {
return snapshots.stream()
.map(snapshot -> snapshot.synchronization().status())
.max(Comparator.comparingInt(Enum::ordinal))
.orElse(SynchronizationStatus.TRUSTED);
}
/** Whether any frame carries degraded or rejected synchronization evidence. */
public boolean hasSynchronizationWarning() {
return worstSynchronizationStatus() != SynchronizationStatus.TRUSTED;
}
/** Mean physical/cycle-consistency score across all processed frames. */
public double meanTdoaConsistencyScore() {
return snapshots.stream()
.mapToDouble(snapshot -> snapshot.tdoaConsistency().consistencyScore())
.average()
.orElse(1.0);
}
/** Largest absolute TDOA cycle residual observed during the run. */
public double maximumTdoaCycleResidualSeconds() {
return snapshots.stream()
.mapToDouble(snapshot -> snapshot.tdoaConsistency().maximumAbsoluteCycleResidualSeconds())
.max()
.orElse(0.0);
}
/** Total pair delays that exceeded their physical propagation limit. */
public int physicalTdoaViolationCount() {
return snapshots.stream()
.mapToInt(snapshot -> snapshot.tdoaConsistency().physicalViolationCount())
.sum();
}
/** Number of frames whose TDOA pair evidence is unreliable. */
public long unreliableTdoaFrameCount() {
return snapshots.stream().filter(snapshot -> !snapshot.tdoaConsistency().reliable()).count();
}
/** Number of frames whose processing time exceeded the configured frame budget. */
public long overBudgetFrameCount() {
if (frameSchedule == null) {
return 0L;
}
long budget = frameSchedule.maxProcessingNanos();
return snapshots.stream().filter(snapshot -> snapshot.processingNanos() > budget).count();
}
/** Whether one snapshot exceeded the configured frame budget. */
public boolean isFrameOverBudget(TrackingSnapshot snapshot) {
Objects.requireNonNull(snapshot, "snapshot");
return frameSchedule != null && snapshot.processingNanos() > frameSchedule.maxProcessingNanos();
}
}