fix: expose physical aliasing and delayed arrivals
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { validateDocumentStructure } from "./schema-validator.js";
|
||||
import {
|
||||
describeFrequencyAliasing,
|
||||
MAX_PRECISE_ANALOG_FREQUENCY_HZ,
|
||||
observeFrequencyBand,
|
||||
} from "../acoustics-core/sampling.js";
|
||||
|
||||
export const SCHEMA_VERSION = "1.0.0";
|
||||
export const INPUT_SCHEMA_VERSION = "1.0.0";
|
||||
export const RESULT_SCHEMA_VERSION = "1.1.0";
|
||||
const MAX_MICROPHONES = 1024;
|
||||
const MAX_SOURCES = 32;
|
||||
const MAX_GRID_SIDE = 512;
|
||||
@@ -18,6 +24,30 @@ function isFiniteNumber(value) {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function samplingMetadataEqual(actual, expected) {
|
||||
if (typeof actual === "number" && typeof expected === "number") {
|
||||
const tolerance =
|
||||
1e-9 + 1e-12 * Math.max(Math.abs(actual), Math.abs(expected));
|
||||
return Number.isFinite(actual) &&
|
||||
Number.isFinite(expected) &&
|
||||
Math.abs(actual - expected) <= tolerance;
|
||||
}
|
||||
if (Array.isArray(actual) || Array.isArray(expected)) {
|
||||
return Array.isArray(actual) &&
|
||||
Array.isArray(expected) &&
|
||||
actual.length === expected.length &&
|
||||
actual.every((value, index) =>
|
||||
samplingMetadataEqual(value, expected[index]));
|
||||
}
|
||||
if (isRecord(actual) || isRecord(expected)) {
|
||||
if (!isRecord(actual) || !isRecord(expected)) return false;
|
||||
const expectedKeys = Object.keys(expected);
|
||||
return expectedKeys.every((key) =>
|
||||
samplingMetadataEqual(actual[key], expected[key]));
|
||||
}
|
||||
return actual === expected;
|
||||
}
|
||||
|
||||
function addIssue(issues, path, message) {
|
||||
issues.push({ path, message });
|
||||
}
|
||||
@@ -92,8 +122,8 @@ function boundsContain(bounds, position) {
|
||||
|
||||
function validateHeader(value, expectedKind, issues) {
|
||||
if (!requireRecord(value, "$", issues)) return false;
|
||||
if (value.schemaVersion !== SCHEMA_VERSION) {
|
||||
addIssue(issues, "$.schemaVersion", `지원 버전은 ${SCHEMA_VERSION}입니다.`);
|
||||
if (value.schemaVersion !== INPUT_SCHEMA_VERSION) {
|
||||
addIssue(issues, "$.schemaVersion", `지원 버전은 ${INPUT_SCHEMA_VERSION}입니다.`);
|
||||
}
|
||||
if (value.kind !== expectedKind) addIssue(issues, "$.kind", `${expectedKind}이어야 합니다.`);
|
||||
requireString(value.id, "$.id", issues);
|
||||
@@ -194,14 +224,14 @@ function validateArrayRelationships(value) {
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateSignal(signal, path, issues, assetsById, sampleRate) {
|
||||
function validateSignal(signal, path, issues, assetsById) {
|
||||
if (!requireRecord(signal, path, issues)) return;
|
||||
if (signal.type === "tone") {
|
||||
if (requireFinite(signal.frequency, `${path}.frequency`, issues, { min: 0, exclusiveMin: true })) {
|
||||
if (signal.frequency >= sampleRate / 2) {
|
||||
addIssue(issues, `${path}.frequency`, `Nyquist 주파수 ${sampleRate / 2} Hz보다 낮아야 합니다.`);
|
||||
}
|
||||
}
|
||||
requireFinite(signal.frequency, `${path}.frequency`, issues, {
|
||||
min: 0,
|
||||
max: MAX_PRECISE_ANALOG_FREQUENCY_HZ,
|
||||
exclusiveMin: true,
|
||||
});
|
||||
if (signal.phaseDeg !== undefined) {
|
||||
requireFinite(signal.phaseDeg, `${path}.phaseDeg`, issues, { min: -360, max: 360 });
|
||||
}
|
||||
@@ -358,7 +388,7 @@ function validateSceneRelationships(value) {
|
||||
if (sceneDurationValid && startValid && durationValid && source.startTime + source.duration > value.duration + 1e-9) {
|
||||
addIssue(issues, path, "음원 구간이 장면 duration을 벗어납니다.");
|
||||
}
|
||||
validateSignal(source.signal, `${path}.signal`, issues, assetsById, sampleRateValid ? value.sampleRate : 48000);
|
||||
validateSignal(source.signal, `${path}.signal`, issues, assetsById);
|
||||
});
|
||||
for (const duplicate of findDuplicates(sourceIds)) addIssue(issues, "$.sources", `중복 음원 id: ${duplicate}`);
|
||||
|
||||
@@ -369,7 +399,7 @@ function validateSceneRelationships(value) {
|
||||
if (!requireRecord(ambient, path, issues)) return;
|
||||
if (requireString(ambient.id, `${path}.id`, issues)) ambientIds.push(ambient.id);
|
||||
requireFinite(ambient.gainDb, `${path}.gainDb`, issues, { min: -120, max: 24 });
|
||||
validateSignal(ambient.signal, `${path}.signal`, issues, assetsById, sampleRateValid ? value.sampleRate : 48000);
|
||||
validateSignal(ambient.signal, `${path}.signal`, issues, assetsById);
|
||||
});
|
||||
for (const duplicate of findDuplicates(ambientIds)) addIssue(issues, "$.ambient", `중복 ambient id: ${duplicate}`);
|
||||
}
|
||||
@@ -412,14 +442,18 @@ function validateExperimentRelationships(value, context = {}) {
|
||||
const path = `$.frequencyBands[${index}]`;
|
||||
if (!requireRecord(band, path, issues)) return;
|
||||
if (requireString(band.id, `${path}.id`, issues)) bandIds.push(band.id);
|
||||
const minValid = requireFinite(band.minHz, `${path}.minHz`, issues, { min: 0 });
|
||||
const maxValid = requireFinite(band.maxHz, `${path}.maxHz`, issues, { min: 0, exclusiveMin: true });
|
||||
const minValid = requireFinite(band.minHz, `${path}.minHz`, issues, {
|
||||
min: 0,
|
||||
max: MAX_PRECISE_ANALOG_FREQUENCY_HZ,
|
||||
});
|
||||
const maxValid = requireFinite(band.maxHz, `${path}.maxHz`, issues, {
|
||||
min: 0,
|
||||
max: MAX_PRECISE_ANALOG_FREQUENCY_HZ,
|
||||
exclusiveMin: true,
|
||||
});
|
||||
if (minValid && maxValid && band.minHz >= band.maxHz) {
|
||||
addIssue(issues, path, "minHz는 maxHz보다 작아야 합니다.");
|
||||
}
|
||||
if (context.scene && maxValid && band.maxHz >= context.scene.sampleRate / 2) {
|
||||
addIssue(issues, `${path}.maxHz`, `Nyquist 주파수 ${context.scene.sampleRate / 2} Hz보다 낮아야 합니다.`);
|
||||
}
|
||||
if (typeof band.color !== "string" || !/^#[0-9a-f]{6}$/i.test(band.color)) {
|
||||
addIssue(issues, `${path}.color`, "#RRGGBB 색상이어야 합니다.");
|
||||
}
|
||||
@@ -436,6 +470,24 @@ function validateExperimentRelationships(value, context = {}) {
|
||||
}
|
||||
if (value.stft.window !== "hann") addIssue(issues, "$.stft.window", "hann이어야 합니다.");
|
||||
}
|
||||
if (
|
||||
context.scene &&
|
||||
Number.isFinite(context.scene.sampleRate) &&
|
||||
Number.isInteger(value.stft?.fftSize) &&
|
||||
Array.isArray(value.frequencyBands)
|
||||
) {
|
||||
value.frequencyBands.forEach((band, index) => {
|
||||
try {
|
||||
observeFrequencyBand(band, context.scene.sampleRate, value.stft.fftSize);
|
||||
} catch (error) {
|
||||
addIssue(
|
||||
issues,
|
||||
`$.frequencyBands[${index}]`,
|
||||
`선택한 FFT grid의 bin center가 없는 대역입니다: ${error.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (requireRecord(value.beamforming, "$.beamforming", issues)) {
|
||||
if (!LOCALIZATION_ALGORITHMS.includes(value.beamforming.algorithm)) {
|
||||
addIssue(
|
||||
@@ -502,7 +554,15 @@ function validateExperimentRelationships(value, context = {}) {
|
||||
"gcc-phat에는 서로 다른 두 개 이상의 마이크가 필요합니다.",
|
||||
);
|
||||
}
|
||||
const retainedBufferCount = context.arrayDefinition.microphones.length + context.scene.sources.length + 1;
|
||||
const sampledSourceCount = context.scene.sources
|
||||
.filter((source) => source.signal?.type !== "tone")
|
||||
.length;
|
||||
const hasSampledAmbient = (context.scene.ambient ?? [])
|
||||
.some((ambient) => ambient.signal?.type !== "tone");
|
||||
const retainedBufferCount =
|
||||
context.arrayDefinition.microphones.length +
|
||||
sampledSourceCount +
|
||||
(hasSampledAmbient ? 1 : 0);
|
||||
const sampleCount = Math.ceil(context.scene.duration * context.scene.sampleRate);
|
||||
let maximumDelaySamples = 0;
|
||||
for (const source of context.scene.sources) {
|
||||
@@ -524,8 +584,15 @@ function validateExperimentRelationships(value, context = {}) {
|
||||
}
|
||||
}
|
||||
let delayFftSize = 1;
|
||||
while (delayFftSize < sampleCount + Math.ceil(maximumDelaySamples) + 2) delayFftSize *= 2;
|
||||
const estimatedBytes = retainedBufferCount * sampleCount * 4 + delayFftSize * 8 * 4;
|
||||
const hasSampledSignal = sampledSourceCount > 0 || hasSampledAmbient;
|
||||
if (hasSampledSignal) {
|
||||
while (delayFftSize < sampleCount + Math.ceil(maximumDelaySamples) + 2) {
|
||||
delayFftSize *= 2;
|
||||
}
|
||||
}
|
||||
const delayWorkingBytes = hasSampledSignal ? delayFftSize * 8 * 4 : 0;
|
||||
const estimatedBytes =
|
||||
retainedBufferCount * sampleCount * 4 + delayWorkingBytes;
|
||||
const maximumBytes = 512 * 1024 * 1024;
|
||||
if (estimatedBytes > maximumBytes) {
|
||||
addIssue(
|
||||
@@ -556,6 +623,13 @@ function validateResultRelationships(value, context = {}) {
|
||||
const sourceIds = new Set(context.scene?.sources?.map((source) => source.id) ?? []);
|
||||
const expectedBandIds = new Set(context.experiment?.frequencyBands?.map((band) => band.id) ?? []);
|
||||
const resultAlgorithm = value.algorithm ?? context.experiment?.beamforming?.algorithm;
|
||||
if (resultAlgorithm === "gcc-phat" && value.backend === "webgpu") {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.backend",
|
||||
"gcc-phat Result must use the CPU backend.",
|
||||
);
|
||||
}
|
||||
const expectedRawScale =
|
||||
resultAlgorithm === "das"
|
||||
? "power"
|
||||
@@ -568,6 +642,201 @@ function validateResultRelationships(value, context = {}) {
|
||||
: expectedRawScale === "score"
|
||||
? "connected-half-maximum-score-weighted-covariance"
|
||||
: undefined;
|
||||
if (
|
||||
Number.isFinite(value.timingMs?.localization) &&
|
||||
Number.isFinite(value.timingMs?.beamforming) &&
|
||||
!samplingMetadataEqual(
|
||||
value.timingMs.localization,
|
||||
value.timingMs.beamforming,
|
||||
)
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.timingMs.localization",
|
||||
"localization must equal the compatibility beamforming timing alias.",
|
||||
);
|
||||
}
|
||||
const expectedObservations = new Map();
|
||||
const expectedEquivalentGroups = new Map();
|
||||
if (
|
||||
context.scene &&
|
||||
context.experiment &&
|
||||
Number.isFinite(context.scene.sampleRate) &&
|
||||
Number.isInteger(context.experiment.stft?.fftSize)
|
||||
) {
|
||||
const definitions = Array.isArray(context.experiment.frequencyBands)
|
||||
? context.experiment.frequencyBands
|
||||
: [];
|
||||
for (const definition of definitions) {
|
||||
try {
|
||||
const observation = observeFrequencyBand(
|
||||
definition,
|
||||
context.scene.sampleRate,
|
||||
context.experiment.stft.fftSize,
|
||||
);
|
||||
expectedObservations.set(definition.id, observation);
|
||||
const group = expectedEquivalentGroups.get(observation.adcBinSetKey) ?? [];
|
||||
group.push(definition.id);
|
||||
expectedEquivalentGroups.set(observation.adcBinSetKey, group);
|
||||
} catch {
|
||||
// Experiment validation reports an unusable band separately.
|
||||
}
|
||||
}
|
||||
}
|
||||
const sampling = value.signalStats?.sampling;
|
||||
const contextSampleRate =
|
||||
Number.isFinite(context.scene?.sampleRate) && context.scene.sampleRate > 0
|
||||
? context.scene.sampleRate
|
||||
: null;
|
||||
const toneDefinitions = new Map();
|
||||
if (context.scene) {
|
||||
const sources = Array.isArray(context.scene.sources) ? context.scene.sources : [];
|
||||
const ambientEntries = Array.isArray(context.scene.ambient)
|
||||
? context.scene.ambient
|
||||
: [];
|
||||
for (const source of sources) {
|
||||
if (source.signal?.type === "tone") {
|
||||
toneDefinitions.set(`source:${source.id}`, source.signal.frequency);
|
||||
}
|
||||
}
|
||||
for (const ambient of ambientEntries) {
|
||||
if (ambient.signal?.type === "tone") {
|
||||
toneDefinitions.set(`ambient:${ambient.id}`, ambient.signal.frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRecord(sampling)) {
|
||||
const samplingRateValid =
|
||||
Number.isFinite(sampling.sampleRate) && sampling.sampleRate > 0;
|
||||
if (
|
||||
samplingRateValid &&
|
||||
!samplingMetadataEqual(sampling.nyquistHz, sampling.sampleRate / 2)
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.signalStats.sampling.nyquistHz",
|
||||
"sampling Nyquist must equal sampling sampleRate / 2.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
contextSampleRate !== null &&
|
||||
!samplingMetadataEqual(sampling.sampleRate, contextSampleRate)
|
||||
) {
|
||||
addIssue(issues, "$.signalStats.sampling.sampleRate", "sampling sampleRate must match Scene.");
|
||||
}
|
||||
if (
|
||||
contextSampleRate !== null &&
|
||||
!samplingMetadataEqual(sampling.nyquistHz, contextSampleRate / 2)
|
||||
) {
|
||||
addIssue(issues, "$.signalStats.sampling.nyquistHz", "sampling Nyquist must equal sampleRate / 2.");
|
||||
}
|
||||
const observedToneKeys = new Set();
|
||||
const toneObservations = Array.isArray(sampling.toneObservations)
|
||||
? sampling.toneObservations
|
||||
: [];
|
||||
for (const [observationIndex, observation] of toneObservations.entries()) {
|
||||
if (!isRecord(observation)) continue;
|
||||
const path = `$.signalStats.sampling.toneObservations[${observationIndex}]`;
|
||||
const observationKey = `${observation.scope}:${observation.id}`;
|
||||
if (observedToneKeys.has(observationKey)) {
|
||||
addIssue(issues, path, `duplicate tone observation: ${observationKey}`);
|
||||
}
|
||||
observedToneKeys.add(observationKey);
|
||||
if (samplingRateValid && Number.isFinite(observation.analogFrequencyHz)) {
|
||||
try {
|
||||
const expected = describeFrequencyAliasing(
|
||||
observation.analogFrequencyHz,
|
||||
sampling.sampleRate,
|
||||
);
|
||||
for (const key of [
|
||||
"analogFrequencyHz",
|
||||
"adcFrequencyHz",
|
||||
"signedAdcFrequencyHz",
|
||||
"aliased",
|
||||
"spectralInversion",
|
||||
"dcEdge",
|
||||
"nyquistEdge",
|
||||
"phaseDegenerate",
|
||||
]) {
|
||||
if (!samplingMetadataEqual(observation[key], expected[key])) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.${key}`,
|
||||
`${key} does not match ideal ADC sampling.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addIssue(issues, `${path}.analogFrequencyHz`, error.message);
|
||||
}
|
||||
}
|
||||
if (context.scene) {
|
||||
const analogFrequencyHz = toneDefinitions.get(observationKey);
|
||||
if (analogFrequencyHz === undefined) {
|
||||
addIssue(issues, path, "tone observation must reference a Scene tone.");
|
||||
} else if (
|
||||
!samplingMetadataEqual(observation.analogFrequencyHz, analogFrequencyHz)
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.analogFrequencyHz`,
|
||||
"analogFrequencyHz must match the Scene tone.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (context.scene) {
|
||||
for (const toneKey of toneDefinitions.keys()) {
|
||||
if (!observedToneKeys.has(toneKey)) {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.signalStats.sampling.toneObservations",
|
||||
`missing Scene tone observation: ${toneKey}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (contextSampleRate !== null) {
|
||||
addIssue(issues, "$.signalStats.sampling", "sampling metadata is required.");
|
||||
}
|
||||
const standaloneObservations = new Map();
|
||||
const standaloneEquivalentGroups = new Map();
|
||||
if (Array.isArray(value.bands)) {
|
||||
value.bands.forEach((band, bandIndex) => {
|
||||
const observation = band?.frequencyObservation;
|
||||
if (!isRecord(observation)) return;
|
||||
if (
|
||||
!Number.isFinite(observation.sampleRate) ||
|
||||
!Number.isInteger(observation.fftSize) ||
|
||||
!Number.isFinite(observation.requestedMinHz) ||
|
||||
!Number.isFinite(observation.requestedMaxHz)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const expected = observeFrequencyBand(
|
||||
{
|
||||
minHz: observation.requestedMinHz,
|
||||
maxHz: observation.requestedMaxHz,
|
||||
},
|
||||
observation.sampleRate,
|
||||
observation.fftSize,
|
||||
);
|
||||
standaloneObservations.set(bandIndex, expected);
|
||||
if (typeof band.id === "string") {
|
||||
const group = standaloneEquivalentGroups.get(expected.adcBinSetKey) ?? [];
|
||||
group.push(band.id);
|
||||
standaloneEquivalentGroups.set(expected.adcBinSetKey, group);
|
||||
}
|
||||
} catch (error) {
|
||||
addIssue(
|
||||
issues,
|
||||
`$.bands[${bandIndex}].frequencyObservation`,
|
||||
`invalid self-described frequency observation: ${error.message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
const bandIds = [];
|
||||
if (Array.isArray(value.bands)) {
|
||||
value.bands.forEach((band, bandIndex) => {
|
||||
@@ -577,7 +846,116 @@ function validateResultRelationships(value, context = {}) {
|
||||
bandIds.push(band.id);
|
||||
if (expectedBandIds.size > 0 && !expectedBandIds.has(band.id)) addIssue(issues, `${path}.id`, "실험에 없는 band id입니다.");
|
||||
}
|
||||
const maps = [band.rawMap, band.displayMap, ...(band.processedMaps?.map((item) => item.map) ?? [])].filter(Boolean);
|
||||
const standaloneObservation = standaloneObservations.get(bandIndex);
|
||||
if (standaloneObservation && isRecord(band.frequencyObservation)) {
|
||||
const observation = band.frequencyObservation;
|
||||
const expectedEquivalentBandIds = (standaloneEquivalentGroups
|
||||
.get(standaloneObservation.adcBinSetKey) ?? [])
|
||||
.filter((candidateId) => candidateId !== band.id)
|
||||
.sort();
|
||||
const expectedFields = {
|
||||
model: standaloneObservation.model,
|
||||
sampleRate: standaloneObservation.sampleRate,
|
||||
nyquistHz: standaloneObservation.nyquistHz,
|
||||
fftSize: standaloneObservation.fftSize,
|
||||
binWidthHz: standaloneObservation.binWidthHz,
|
||||
requestedMinHz: standaloneObservation.requestedMinHz,
|
||||
requestedMaxHz: standaloneObservation.requestedMaxHz,
|
||||
aliased: standaloneObservation.aliased,
|
||||
crossesFirstNyquist: standaloneObservation.crossesFirstNyquist,
|
||||
localizationInputModel: standaloneObservation.localizationInputModel,
|
||||
selectionModel: standaloneObservation.selectionModel,
|
||||
localizationModel:
|
||||
resultAlgorithm === "gcc-phat"
|
||||
? "band-limited-gcc-phat-geometric-lag"
|
||||
: "spherical-steering-at-positive-adc-bin-frequency",
|
||||
originalCarrierUsed: standaloneObservation.originalCarrierUsed,
|
||||
aliasOrderResolution:
|
||||
standaloneObservation.aliased ? "not-attempted" : "not-applicable",
|
||||
foldBoundaryCount: standaloneObservation.foldBoundaryCount,
|
||||
continuousManyToOne: standaloneObservation.continuousManyToOne,
|
||||
gridManyToOne: standaloneObservation.gridManyToOne,
|
||||
adcBinSetKey: standaloneObservation.adcBinSetKey,
|
||||
};
|
||||
for (const [key, expected] of Object.entries(expectedFields)) {
|
||||
if (!samplingMetadataEqual(observation[key], expected)) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation.${key}`,
|
||||
`${key} is not self-consistent with the Result sampling inputs.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const [key, expected] of [
|
||||
["adcBins", standaloneObservation.bins],
|
||||
["adcBinCenterRanges", standaloneObservation.adcBinCenterRanges],
|
||||
["foldSegments", standaloneObservation.foldSegments],
|
||||
["equivalentBandIds", expectedEquivalentBandIds],
|
||||
]) {
|
||||
if (!samplingMetadataEqual(observation[key], expected)) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation.${key}`,
|
||||
`${key} does not match the deterministic ADC fold.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isRecord(sampling) &&
|
||||
Number.isFinite(sampling.sampleRate) &&
|
||||
!samplingMetadataEqual(observation.sampleRate, sampling.sampleRate)
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation.sampleRate`,
|
||||
"band sampleRate must match signalStats.sampling.sampleRate.",
|
||||
);
|
||||
}
|
||||
} else if (!isRecord(band.frequencyObservation)) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation`,
|
||||
"frequency observation metadata is required for every Result band.",
|
||||
);
|
||||
}
|
||||
const contextObservation = expectedObservations.get(band.id);
|
||||
if (contextObservation && isRecord(band.frequencyObservation)) {
|
||||
const observation = band.frequencyObservation;
|
||||
for (const [key, expected] of Object.entries({
|
||||
sampleRate: contextObservation.sampleRate,
|
||||
fftSize: contextObservation.fftSize,
|
||||
requestedMinHz: contextObservation.requestedMinHz,
|
||||
requestedMaxHz: contextObservation.requestedMaxHz,
|
||||
})) {
|
||||
if (!samplingMetadataEqual(observation[key], expected)) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation.${key}`,
|
||||
`${key} does not match Scene/Experiment.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const expectedEquivalentBandIds = expectedEquivalentGroups
|
||||
.get(contextObservation.adcBinSetKey)
|
||||
.filter((candidateId) => candidateId !== band.id)
|
||||
.sort();
|
||||
if (
|
||||
!samplingMetadataEqual(
|
||||
observation.equivalentBandIds,
|
||||
expectedEquivalentBandIds,
|
||||
)
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${path}.frequencyObservation.equivalentBandIds`,
|
||||
"equivalentBandIds does not match the Experiment band set.",
|
||||
);
|
||||
}
|
||||
}
|
||||
const processedMaps = Array.isArray(band.processedMaps)
|
||||
? band.processedMaps.map((item) => item?.map)
|
||||
: [];
|
||||
const maps = [band.rawMap, band.displayMap, ...processedMaps].filter(Boolean);
|
||||
maps.forEach((map, mapIndex) => {
|
||||
if (!isRecord(map) || !Array.isArray(map.values)) return;
|
||||
if (Number.isInteger(map.rows) && Number.isInteger(map.columns) && map.values.length !== map.rows * map.columns) {
|
||||
@@ -620,6 +998,11 @@ function validateResultRelationships(value, context = {}) {
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const expectedBandId of expectedBandIds) {
|
||||
if (!bandIds.includes(expectedBandId)) {
|
||||
addIssue(issues, "$.bands", `missing Experiment band result: ${expectedBandId}`);
|
||||
}
|
||||
}
|
||||
for (const duplicate of findDuplicates(bandIds)) addIssue(issues, "$.bands", `중복 result band id: ${duplicate}`);
|
||||
return issues;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user