feat: harden end-to-end simulation workflow

This commit is contained in:
Codex
2026-08-28 12:34:11 +09:00
parent 2ce96064cf
commit 3317a03d1a
17 changed files with 577 additions and 24 deletions
+23
View File
@@ -5,6 +5,7 @@ export const RESULT_METRIC_KEYS = Object.freeze([
"energyInsideTruthRatio",
"precision",
"recall",
"mainLobeWidthM",
]);
const RESULT_METRIC_KEY_SET = new Set(RESULT_METRIC_KEYS);
@@ -457,6 +458,8 @@ export function evaluateDasBand(options) {
minimumPeakDistanceM,
sidelobeExclusionRadiusM,
halfPowerRatio,
dynamicRangeDb,
maximumDetections,
},
metrics: {
peakToSidelobeDb: peakToSidelobeDb(
@@ -470,6 +473,7 @@ export function evaluateDasBand(options) {
energyInsideTruthRatio: energyInsideRatio(values, scan, truths, truthRadiusM, globalPeak?.value ?? 0),
precision,
recall,
mainLobeWidthM: mainLobe?.equivalentDiameterM ?? 0,
},
diagnostics: {
mainLobe,
@@ -513,11 +517,30 @@ export function exportBandEvaluation(evaluation, options = {}) {
if (match) {
resultPeak.sourceId = match.sourceId;
resultPeak.localizationErrorM = match.localizationErrorM;
const resolution = evaluation.diagnostics?.gridResolutionM;
if (resolution?.x > 0 && resolution?.y > 0) {
resultPeak.localizationErrorPixels = Math.hypot(
(match.peak.position[0] - match.truthPosition[0]) / resolution.x,
(match.peak.position[1] - match.truthPosition[1]) / resolution.y,
);
}
}
return resultPeak;
});
return {
peaks,
metrics: selectResultMetrics(evaluation, options.metricNames ?? RESULT_METRIC_KEYS),
evaluation: {
version: "1.0.0",
truthRadiusM: evaluation.parameters.truthRadiusM,
matchToleranceM: evaluation.parameters.matchToleranceM,
detectionThresholdRatio: evaluation.parameters.detectionThresholdRatio,
detectionThresholdValue: evaluation.parameters.detectionThresholdValue,
minimumPeakDistanceM: evaluation.parameters.minimumPeakDistanceM,
sidelobeExclusionRadiusM: evaluation.parameters.sidelobeExclusionRadiusM,
halfPowerRatio: evaluation.parameters.halfPowerRatio,
dynamicRangeDb: evaluation.parameters.dynamicRangeDb,
maximumDetections: evaluation.parameters.maximumDetections,
},
};
}
+8 -6
View File
@@ -1,12 +1,14 @@
import { detectWebGpuCapability } from "./webgpu-status.js";
const statusElement = document.querySelector("#webgpu-status");
if (!(statusElement instanceof HTMLElement)) {
throw new Error("WebGPU status element is missing.");
}
if ("gpu" in navigator) {
statusElement.textContent = "WebGPU 사용 가능";
} else {
statusElement.textContent = "CPU fallback 사용 예정";
statusElement.classList.add("status-chip--warning");
}
detectWebGpuCapability().then((capability) => {
statusElement.textContent = capability.label;
statusElement.title = capability.detail;
statusElement.classList.toggle("status-chip--warning", !capability.available);
statusElement.dataset.backendAvailable = capability.available ? "webgpu" : "cpu";
});
+12 -7
View File
@@ -1,8 +1,13 @@
const statusElements = document.querySelectorAll("[data-runtime-status]");
const hasWebGpu = "gpu" in navigator;
import { detectWebGpuCapability } from "./webgpu-status.js";
for (const statusElement of statusElements) {
statusElement.textContent = hasWebGpu ? "WebGPU 준비됨" : "CPU fallback";
statusElement.classList.toggle("status-chip--warning", !hasWebGpu);
statusElement.classList.toggle("status-chip--neutral", hasWebGpu);
}
const statusElements = document.querySelectorAll("[data-runtime-status]");
detectWebGpuCapability().then((capability) => {
for (const statusElement of statusElements) {
statusElement.textContent = capability.label;
statusElement.title = capability.detail;
statusElement.classList.toggle("status-chip--warning", !capability.available);
statusElement.classList.toggle("status-chip--neutral", capability.available);
statusElement.dataset.backendAvailable = capability.available ? "webgpu" : "cpu";
}
});
+33
View File
@@ -0,0 +1,33 @@
let capabilityPromise;
export function detectWebGpuCapability() {
capabilityPromise ??= detect();
return capabilityPromise;
}
async function detect() {
if (!navigator.gpu) {
return { available: false, label: "CPU fallback", detail: "WebGPU API 없음" };
}
try {
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
if (!adapter) {
return { available: false, label: "CPU fallback", detail: "WebGPU adapter 없음" };
}
const info = adapter.info;
const adapterName = [info?.vendor, info?.architecture || info?.device]
.filter(Boolean)
.join(" · ");
return {
available: true,
label: adapterName ? `WebGPU · ${adapterName}` : "WebGPU adapter 준비됨",
detail: adapterName || "WebGPU adapter 준비됨",
};
} catch (error) {
return {
available: false,
label: "CPU fallback",
detail: error instanceof Error ? error.message : String(error),
};
}
}