feat: add physically explicit narrowband MUSIC
This commit is contained in:
+174
-15
@@ -1,5 +1,17 @@
|
||||
import { getFrameCount, binsForBand } from "/packages/acoustics-core/stft.js";
|
||||
import { observeFrequencyBand } from "/packages/acoustics-core/sampling.js";
|
||||
import {
|
||||
binsForBand,
|
||||
getCompleteFrameCount,
|
||||
getFrameCount,
|
||||
} from "/packages/acoustics-core/stft.js";
|
||||
import {
|
||||
DEFAULT_MAXIMUM_MUSIC_COVARIANCE_COMPLEX_ACCUMULATIONS,
|
||||
DEFAULT_MAXIMUM_MUSIC_JACOBI_PAIR_UPDATES,
|
||||
DEFAULT_MAXIMUM_MUSIC_PROJECTION_COMPLEX_MACS,
|
||||
} from "/packages/acoustics-core/music.js";
|
||||
import {
|
||||
describeFrequencyAliasing,
|
||||
observeFrequencyBand,
|
||||
} from "/packages/acoustics-core/sampling.js";
|
||||
import {
|
||||
DEFAULT_LOCALIZATION_ALGORITHM,
|
||||
localizationAlgorithmDefinition,
|
||||
@@ -35,9 +47,16 @@ export function createComparisonSignature(scene, sourceExperiment) {
|
||||
if (!scene || !sourceExperiment) return "";
|
||||
const experiment = structuredClone(sourceExperiment);
|
||||
experiment.arrayId = "$array";
|
||||
experiment.sceneId = "$scene";
|
||||
if (experiment.beamforming) {
|
||||
experiment.beamforming.algorithm = "$algorithm";
|
||||
experiment.beamforming.backend = "$backend";
|
||||
experiment.beamforming.normalization =
|
||||
"$algorithm-specific-normalization";
|
||||
delete experiment.beamforming.musicSourceCountMethod;
|
||||
delete experiment.beamforming.musicSourceCount;
|
||||
delete experiment.beamforming.phatRelativeFloor;
|
||||
delete experiment.beamforming.phatFloorRatio;
|
||||
}
|
||||
return canonicalStringify({ scene, experiment });
|
||||
}
|
||||
@@ -51,19 +70,39 @@ export async function hashSimulationInputs({ arrayDefinition, scene, experiment
|
||||
}
|
||||
|
||||
export function estimateExperimentWorkload(arrayDefinition, scene, experiment) {
|
||||
const frameCount = getFrameCount(
|
||||
Math.ceil(scene.duration * scene.sampleRate),
|
||||
experiment.stft.fftSize,
|
||||
experiment.stft.hopSize,
|
||||
);
|
||||
const sampleCount = Math.ceil(scene.duration * scene.sampleRate);
|
||||
const algorithm =
|
||||
experiment.beamforming?.algorithm ?? DEFAULT_LOCALIZATION_ALGORITHM;
|
||||
const musicSourceCountMethod =
|
||||
experiment.beamforming?.musicSourceCountMethod ?? "fixed";
|
||||
const frameCount =
|
||||
algorithm === "music" && musicSourceCountMethod !== "fixed"
|
||||
? getCompleteFrameCount(
|
||||
sampleCount,
|
||||
experiment.stft.fftSize,
|
||||
experiment.stft.hopSize,
|
||||
)
|
||||
: getFrameCount(
|
||||
sampleCount,
|
||||
experiment.stft.fftSize,
|
||||
experiment.stft.hopSize,
|
||||
);
|
||||
const bandBins = experiment.frequencyBands.map((band) =>
|
||||
binsForBand(band, scene.sampleRate, experiment.stft.fftSize),
|
||||
);
|
||||
const bandBinSelectionCount = bandBins.reduce((total, bins) => total + bins.length, 0);
|
||||
const selectedBinCount = new Set(bandBins.flat()).size;
|
||||
const binWidthHz =
|
||||
scene.sampleRate / experiment.stft.fftSize;
|
||||
const musicHypothesisCount = new Set(
|
||||
experiment.frequencyBands.map((band, bandIndex) => {
|
||||
const analogBin = Math.ceil(band.minHz / binWidthHz);
|
||||
const adcBin = bandBins[bandIndex]?.[0];
|
||||
return `${analogBin}|${adcBin}`;
|
||||
}),
|
||||
).size;
|
||||
const pointCount = experiment.scan.columns * experiment.scan.rows;
|
||||
const microphoneCount = arrayDefinition.microphones.length;
|
||||
const algorithm = experiment.beamforming?.algorithm ?? DEFAULT_LOCALIZATION_ALGORITHM;
|
||||
const algorithmDefinition = localizationAlgorithmDefinition(algorithm);
|
||||
const referencePairCount = Math.max(0, microphoneCount - 1);
|
||||
const stftTransformCount = frameCount * microphoneCount;
|
||||
@@ -72,7 +111,50 @@ export function estimateExperimentWorkload(arrayDefinition, scene, experiment) {
|
||||
2 * referencePairCount * frameCount * bandBinSelectionCount;
|
||||
const gccScanScoreOps = pointCount * referencePairCount * bandBins.length;
|
||||
const inverseFftTransformCount = referencePairCount * bandBins.length;
|
||||
const scanOperationCount = algorithm === "gcc-phat" ? gccScanScoreOps : dasComplexMacCount;
|
||||
const musicSourceCount = experiment.beamforming?.musicSourceCount ?? 1;
|
||||
if (
|
||||
algorithm === "music" &&
|
||||
musicSourceCountMethod === "fixed" &&
|
||||
(
|
||||
!Number.isInteger(musicSourceCount) ||
|
||||
musicSourceCount < 1 ||
|
||||
musicSourceCount >= microphoneCount
|
||||
)
|
||||
) {
|
||||
throw new RangeError(
|
||||
`MUSIC source count K must be inside 1..${microphoneCount - 1}.`,
|
||||
);
|
||||
}
|
||||
const musicNoiseSubspaceDimension =
|
||||
musicSourceCountMethod === "fixed"
|
||||
? microphoneCount - musicSourceCount
|
||||
: microphoneCount;
|
||||
const musicCovarianceComplexAccumulations =
|
||||
musicHypothesisCount *
|
||||
frameCount *
|
||||
microphoneCount *
|
||||
(microphoneCount + 1) /
|
||||
2;
|
||||
const musicJacobiWorstCasePairUpdates =
|
||||
musicHypothesisCount *
|
||||
32 *
|
||||
(microphoneCount * (microphoneCount - 1) / 2) *
|
||||
(2 * microphoneCount - 2);
|
||||
const musicProjectionComplexMacs =
|
||||
musicHypothesisCount *
|
||||
pointCount *
|
||||
microphoneCount *
|
||||
musicNoiseSubspaceDimension;
|
||||
const musicTotalOperationCount =
|
||||
musicCovarianceComplexAccumulations +
|
||||
musicJacobiWorstCasePairUpdates +
|
||||
musicProjectionComplexMacs;
|
||||
const scanOperationCount =
|
||||
algorithm === "gcc-phat"
|
||||
? gccScanScoreOps
|
||||
: algorithm === "music"
|
||||
? musicTotalOperationCount
|
||||
: dasComplexMacCount;
|
||||
return {
|
||||
algorithm,
|
||||
algorithmLabel: algorithmDefinition?.label ?? algorithm,
|
||||
@@ -87,12 +169,42 @@ export function estimateExperimentWorkload(arrayDefinition, scene, experiment) {
|
||||
stftTransformCount,
|
||||
referencePairCount,
|
||||
scanOperationCount,
|
||||
scanOperationKind: algorithm === "gcc-phat" ? "reference-pair lookup" : "complex MAC",
|
||||
scanOperationKind:
|
||||
algorithm === "gcc-phat"
|
||||
? "reference-pair lookup"
|
||||
: algorithm === "music"
|
||||
? "covariance + Jacobi upper bound + projection"
|
||||
: "complex MAC",
|
||||
gccCrossSpectrumComplexOps: algorithm === "gcc-phat" ? gccCrossSpectrumComplexOps : 0,
|
||||
inverseFftTransformCount: algorithm === "gcc-phat" ? inverseFftTransformCount : 0,
|
||||
complexMacCount: algorithm === "gcc-phat" ? 0 : dasComplexMacCount,
|
||||
complexMacCount:
|
||||
algorithm === "gcc-phat" || algorithm === "music"
|
||||
? 0
|
||||
: dasComplexMacCount,
|
||||
musicSourceCountMethod,
|
||||
musicSourceCount,
|
||||
musicHypothesisCount,
|
||||
musicNoiseSubspaceDimension,
|
||||
musicCovarianceComplexAccumulations:
|
||||
algorithm === "music" ? musicCovarianceComplexAccumulations : 0,
|
||||
musicJacobiWorstCasePairUpdates:
|
||||
algorithm === "music" ? musicJacobiWorstCasePairUpdates : 0,
|
||||
musicProjectionComplexMacs:
|
||||
algorithm === "music" ? musicProjectionComplexMacs : 0,
|
||||
musicJacobiPairUpdateLimit:
|
||||
DEFAULT_MAXIMUM_MUSIC_JACOBI_PAIR_UPDATES,
|
||||
musicCovarianceAccumulationLimit:
|
||||
DEFAULT_MAXIMUM_MUSIC_COVARIANCE_COMPLEX_ACCUMULATIONS,
|
||||
musicProjectionMacLimit:
|
||||
DEFAULT_MAXIMUM_MUSIC_PROJECTION_COMPLEX_MACS,
|
||||
musicJacobiLimitExceeded:
|
||||
algorithm === "music" &&
|
||||
musicJacobiWorstCasePairUpdates >
|
||||
DEFAULT_MAXIMUM_MUSIC_JACOBI_PAIR_UPDATES,
|
||||
scalingVs50: pointCount / 2500,
|
||||
distanceMiB: (pointCount * microphoneCount * 4) / 1024 ** 2,
|
||||
distanceMiB:
|
||||
(pointCount * microphoneCount * (algorithm === "music" ? 8 : 4)) /
|
||||
1024 ** 2,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +357,9 @@ function environmentForResult({ arrayDefinition, experiment, run }) {
|
||||
if (adapter?.architecture) environment.gpuArchitecture = adapter.architecture;
|
||||
if (adapter?.device) environment.gpuDevice = adapter.device;
|
||||
if (run.gpu?.timingKind) environment.gpuTimingKind = run.gpu.timingKind;
|
||||
if (Number.isInteger(run.frameCount) && run.frameCount >= 0) {
|
||||
environment.stftFrameCount = run.frameCount;
|
||||
}
|
||||
const referenceMic =
|
||||
run.referenceMic ??
|
||||
run.referenceMicrophoneId ??
|
||||
@@ -291,6 +406,18 @@ function frequencyObservationForResult(scene, experiment, band, algorithm) {
|
||||
scene.sampleRate,
|
||||
experiment.stft.fftSize,
|
||||
);
|
||||
const music = algorithm === "music";
|
||||
const analogBin = Math.ceil(
|
||||
band.minHz / observation.binWidthHz,
|
||||
);
|
||||
const configuredAnalogFrequencyHz =
|
||||
analogBin * observation.binWidthHz;
|
||||
const configuredAlias = music
|
||||
? describeFrequencyAliasing(
|
||||
configuredAnalogFrequencyHz,
|
||||
scene.sampleRate,
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
model: observation.model,
|
||||
sampleRate: observation.sampleRate,
|
||||
@@ -306,9 +433,28 @@ function frequencyObservationForResult(scene, experiment, band, algorithm) {
|
||||
localizationModel:
|
||||
algorithm === "gcc-phat"
|
||||
? "band-limited-gcc-phat-geometric-lag"
|
||||
: music
|
||||
? "near-field-music-at-configured-analog-frequency"
|
||||
: "spherical-steering-at-positive-adc-bin-frequency",
|
||||
originalCarrierUsed: observation.originalCarrierUsed,
|
||||
aliasOrderResolution: observation.aliased ? "not-attempted" : "not-applicable",
|
||||
originalCarrierUsed: music,
|
||||
aliasOrderResolution:
|
||||
music && observation.aliased
|
||||
? "configured-single-hypothesis"
|
||||
: observation.aliased
|
||||
? "not-attempted"
|
||||
: "not-applicable",
|
||||
...(music
|
||||
? {
|
||||
configuredAnalogFrequencyHz,
|
||||
signedAdcFrequencyHz:
|
||||
configuredAlias.signedAdcFrequencyHz,
|
||||
spectralInversion:
|
||||
configuredAlias.spectralInversion,
|
||||
frequencyHypothesisCount: 1,
|
||||
frequencyHypothesisSource:
|
||||
"experiment-band-not-scene-truth",
|
||||
}
|
||||
: {}),
|
||||
foldBoundaryCount: observation.foldBoundaryCount,
|
||||
continuousManyToOne: observation.continuousManyToOne,
|
||||
gridManyToOne: observation.gridManyToOne,
|
||||
@@ -387,21 +533,31 @@ export function createResultDocument(options) {
|
||||
beamforming: run.timingMs.beamforming ?? run.timingMs.localization,
|
||||
},
|
||||
environment: environmentForResult({ arrayDefinition, experiment, run }),
|
||||
...(algorithm === "music" && run.model
|
||||
? { musicModel: sortForCanonicalJson(run.model) }
|
||||
: {}),
|
||||
bands: run.bands.map((band) => {
|
||||
const evaluation = evaluations.get(band.id) ?? { peaks: [], metrics: {} };
|
||||
const bandRawScale = band.rawScale ?? rawScale;
|
||||
const objectiveDirection =
|
||||
band.objectiveDirection ??
|
||||
(bandRawScale === "noise-projection" ? "minimum" : "maximum");
|
||||
const bandResult = {
|
||||
id: band.id,
|
||||
color: band.color,
|
||||
rawMap: {
|
||||
rows: experiment.scan.rows,
|
||||
columns: experiment.scan.columns,
|
||||
scale: band.rawScale ?? rawScale,
|
||||
scale: bandRawScale,
|
||||
objectiveDirection,
|
||||
values: Array.from(band.rawMap),
|
||||
},
|
||||
displayMap: {
|
||||
rows: experiment.scan.rows,
|
||||
columns: experiment.scan.columns,
|
||||
scale: band.displayScale,
|
||||
objectiveDirection:
|
||||
band.displayObjectiveDirection ?? objectiveDirection,
|
||||
values: Array.from(band.displayMap),
|
||||
},
|
||||
peaks: evaluation.peaks.map(peakForResult),
|
||||
@@ -409,6 +565,9 @@ export function createResultDocument(options) {
|
||||
};
|
||||
const frequencyObservation = frequencyObservations.get(band.id);
|
||||
if (frequencyObservation) bandResult.frequencyObservation = frequencyObservation;
|
||||
if (algorithm === "music" && band.diagnostics) {
|
||||
bandResult.musicDiagnostics = sortForCanonicalJson(band.diagnostics);
|
||||
}
|
||||
if (evaluation.evaluation) {
|
||||
bandResult.evaluation = {
|
||||
...evaluation.evaluation,
|
||||
|
||||
Reference in New Issue
Block a user