import { auditCovarianceEigenvalues, computeComplexSampleCovariance, estimateComplexCovarianceWorkload, selectWaxKailathSourceCount, } from "./covariance.js"; import { cooperativeYield } from "./cooperative-yield.js"; import { DEFAULT_MAX_COMPENSATION_DB, interpolateFrequencyResponse, inverseResponseGainDb, requireMaxCompensationDb, } from "./frequency-response.js"; import { DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS, decomposeHermitian, estimateHermitianEigenWorkload, } from "./hermitian-eigen.js"; import { dbToLinear, scanIndexToPosition, TAU } from "./math.js"; import { describeFrequencyAliasing } from "./sampling.js"; import { requireLocalizationAlgorithmDefinition } from "./localization-algorithms.js"; import { binsForBand, createStftWorkspace, extractStftBins, getCompleteFrameCount, getFrameCount, } from "./stft.js"; const FLOAT64_BYTES = Float64Array.BYTES_PER_ELEMENT; const MUSIC_ANALYSIS_PLAN_KIND = "micarrsim.music-analysis-plan/1"; export class MusicUnavailableError extends Error { constructor(reason, message, details = {}) { super(message); this.name = "MusicUnavailableError"; this.code = "MUSIC_UNAVAILABLE"; this.reason = reason; this.details = Object.freeze({ ...details }); } } export function isMusicUnavailableError(error) { return ( error instanceof MusicUnavailableError || ( error?.code === "MUSIC_UNAVAILABLE" && typeof error?.reason === "string" ) ); } export const DEFAULT_MAXIMUM_MUSIC_WORKING_BYTES = 512 * 1024 * 1024; export const DEFAULT_MAXIMUM_MUSIC_PLAN_BYTES = 512 * 1024 * 1024; export const DEFAULT_MAXIMUM_MUSIC_COVARIANCE_COMPLEX_ACCUMULATIONS = 1_000_000_000; export const DEFAULT_MAXIMUM_MUSIC_PROJECTION_COMPLEX_MACS = 1_000_000_000; export const DEFAULT_MAXIMUM_MUSIC_JACOBI_PAIR_UPDATES = 300_000_000; export const DEFAULT_MAXIMUM_MUSIC_FFT_BUTTERFLIES = 1_000_000_000; function createAbortError(signal) { if (signal?.reason instanceof Error && signal.reason.name === "AbortError") { return signal.reason; } const error = new Error("MUSIC localization was canceled."); error.name = "AbortError"; return error; } function throwIfAborted(signal) { if (signal?.aborted) throw createAbortError(signal); } function requirePositiveFinite(value, label) { if (!Number.isFinite(value) || value <= 0) { throw new RangeError(`${label} must be a positive finite number.`); } return value; } function requireNonNegativeFinite(value, label) { if (!Number.isFinite(value) || value < 0) { throw new RangeError(`${label} must be a non-negative finite number.`); } return value; } function requirePositiveSafeInteger(value, label) { if (!Number.isSafeInteger(value) || value < 1) { throw new RangeError(`${label} must be a positive safe integer.`); } return value; } function checkedProduct(factors, label) { let product = 1; for (const factor of factors) { if (!Number.isSafeInteger(factor) || factor < 0) { throw new RangeError(`${label} has an invalid factor: ${factor}.`); } if (factor !== 0 && product > Number.MAX_SAFE_INTEGER / factor) { throw new RangeError(`${label} exceeds the safe integer range.`); } product *= factor; } return product; } function checkedSum(values, label) { let sum = 0; for (const value of values) { if ( !Number.isSafeInteger(value) || value < 0 || sum > Number.MAX_SAFE_INTEGER - value ) { throw new RangeError(`${label} exceeds the safe integer range.`); } sum += value; } return sum; } function requireMusicAlgorithm(experiment) { const algorithm = requireLocalizationAlgorithmDefinition( experiment?.beamforming?.algorithm, ); if (algorithm.id !== "music") { throw new RangeError(`MUSIC implementation cannot run ${algorithm.id}.`); } return algorithm; } function requireFftConfiguration(experiment) { const fftSize = experiment?.stft?.fftSize; const hopSize = experiment?.stft?.hopSize; if ( !Number.isSafeInteger(fftSize) || fftSize < 2 || (fftSize & (fftSize - 1)) !== 0 ) { throw new RangeError( "experiment.stft.fftSize must be a power of two greater than one.", ); } if (!Number.isSafeInteger(hopSize) || hopSize < 1 || hopSize > fftSize) { throw new RangeError( "experiment.stft.hopSize must be an integer inside 1..fftSize.", ); } return { fftSize, hopSize }; } function requireScan(scan) { if (!scan || typeof scan !== "object") { throw new TypeError("experiment.scan must be an object."); } for (const field of ["xMin", "xMax", "yMin", "yMax", "z"]) { if (!Number.isFinite(scan[field])) { throw new TypeError(`experiment.scan.${field} must be finite.`); } } requirePositiveSafeInteger(scan.columns, "experiment.scan.columns"); requirePositiveSafeInteger(scan.rows, "experiment.scan.rows"); if (scan.xMax < scan.xMin || scan.yMax < scan.yMin) { throw new RangeError("experiment.scan bounds must satisfy min <= max."); } checkedProduct([scan.columns, scan.rows], "MUSIC scan point count"); return scan; } function resolveSourceCountConfiguration( beamforming, microphoneCount, hopSize, fftSize, ) { const method = beamforming.musicSourceCountMethod ?? "fixed"; if (method !== "fixed" && method !== "mdl" && method !== "aic") { throw new RangeError( 'musicSourceCountMethod must be "fixed", "mdl", or "aic".', ); } if (method === "fixed") { const value = beamforming.musicSourceCount; if (!Number.isSafeInteger(value) || value < 1 || value >= microphoneCount) { throw new RangeError( `beamforming.musicSourceCount must be an integer inside 1..${microphoneCount - 1}.`, ); } return Object.freeze({ method, value }); } if (hopSize !== fftSize) { throw new RangeError( `MUSIC ${method.toUpperCase()} source-count selection requires non-overlapping ` + "STFT snapshots (hopSize === fftSize); no independence correction is invented.", ); } return Object.freeze({ method }); } function resolveBandBins(frequencyBands, sampleRate, fftSize) { if (!Array.isArray(frequencyBands) || frequencyBands.length === 0) { throw new TypeError("experiment.frequencyBands must be a non-empty array."); } const binWidthHz = sampleRate / fftSize; const bandHypotheses = frequencyBands.map((band, bandIndex) => { const bins = binsForBand(band, sampleRate, fftSize); const label = band?.id ?? `index ${bandIndex}`; if (bins.length !== 1) { throw new RangeError( `MUSIC band ${label} must select exactly one observed positive ADC FFT bin; ` + `it selects ${bins.length} bins [${bins.join(", ")}]. ` + "This implementation does not average or fuse multiple bins.", ); } const firstAnalogBin = Math.ceil(band.minHz / binWidthHz); const lastAnalogBin = Math.floor(band.maxHz / binWidthHz); if (firstAnalogBin !== lastAnalogBin) { const hypothesisCount = lastAnalogBin - firstAnalogBin + 1; throw new RangeError( `MUSIC band ${label} contains ${hypothesisCount} analog FFT-grid hypotheses; ` + "standard narrowband MUSIC returns one map per configured carrier. " + "Narrow the band to one analog FFT-grid center or create separate bands. " + "No alias-order choice, averaging, or maximum fusion is applied.", ); } const bin = bins[0]; if (bin === 0 || bin === fftSize / 2) { const edge = bin === 0 ? "DC" : "Nyquist"; throw new RangeError( `MUSIC band ${label} selects the exact ${edge} bin ${bin}; ` + "a real ADC has no independent quadrature component there, so " + "standard complex narrowband MUSIC is phase-degenerate.", ); } const analogFrequencyHz = firstAnalogBin * binWidthHz; const alias = describeFrequencyAliasing( analogFrequencyHz, sampleRate, ); const signedAdcBin = Math.round( alias.signedAdcFrequencyHz / binWidthHz, ); if (Math.abs(signedAdcBin) !== bin) { throw new Error( `MUSIC band ${label} produced an inconsistent analog/ADC FFT-grid mapping.`, ); } const aliasOrder = Math.round( (analogFrequencyHz - alias.signedAdcFrequencyHz) / sampleRate, ); const hypothesisKey = `analog-bin:${firstAnalogBin}|adc-bin:${bin}`; return Object.freeze({ bandIndex, bandId: band.id, hypothesisKey, analogBin: firstAnalogBin, analogFrequencyHz, bin, observedFrequencyHz: bin * binWidthHz, signedAdcBin, signedAdcFrequencyHz: alias.signedAdcFrequencyHz, spectralInversion: alias.spectralInversion, orientationSign: alias.spectralInversion ? -1 : 1, aliasOrder, aliased: alias.aliased, sharedObservationKey: `fs:${sampleRate}|fft:${fftSize}|bin:${bin}`, }); }); const bandBins = bandHypotheses.map(({ bin }) => Object.freeze([bin])); const selectedBins = Object.freeze( [...new Set(bandBins.map(([bin]) => bin))].sort( (left, right) => left - right, ), ); const uniqueHypotheses = new Map(); for (const hypothesis of bandHypotheses) { if (!uniqueHypotheses.has(hypothesis.hypothesisKey)) { uniqueHypotheses.set(hypothesis.hypothesisKey, hypothesis); } } const analysisHypotheses = Object.freeze( [...uniqueHypotheses.values()], ); const hypothesesByBin = new Map(); for (const hypothesis of analysisHypotheses) { const group = hypothesesByBin.get(hypothesis.bin) ?? []; group.push(hypothesis); hypothesesByBin.set(hypothesis.bin, group); } return { bandBins: Object.freeze(bandBins), bandHypotheses: Object.freeze(bandHypotheses), analysisHypotheses, hypothesesByBin, selectedBins, }; } function resolveConfiguration(options) { const { sampleRate, arrayDefinition, scene, experiment } = options; requirePositiveFinite(sampleRate, "sampleRate"); if ( !Array.isArray(arrayDefinition?.microphones) || arrayDefinition.microphones.length < 2 ) { throw new RangeError("MUSIC requires at least two microphones."); } const microphoneCount = arrayDefinition.microphones.length; requirePositiveSafeInteger(microphoneCount, "microphone count"); requirePositiveFinite(scene?.speedOfSound, "scene.speedOfSound"); const algorithm = requireMusicAlgorithm(experiment); const { fftSize, hopSize } = requireFftConfiguration(experiment); const scan = requireScan(experiment.scan); const pointCount = checkedProduct( [scan.columns, scan.rows], "MUSIC scan point count", ); const { bandBins, bandHypotheses, analysisHypotheses, hypothesesByBin, selectedBins, } = resolveBandBins( experiment.frequencyBands, sampleRate, fftSize, ); const beamforming = experiment.beamforming; if (beamforming.normalization !== "none") { throw new RangeError( 'MUSIC requires beamforming.normalization === "none"; raw q is never peak-normalized or inverted.', ); } const minimumDistance = requirePositiveFinite( beamforming.minimumDistance ?? 0.01, "beamforming.minimumDistance", ); if (!Number.isFinite(1 / minimumDistance)) { throw new RangeError( "beamforming.minimumDistance is too small for finite inverse-distance steering.", ); } const compensateFrequencyResponse = beamforming.compensateFrequencyResponse !== false; const maxCompensationDb = requireMaxCompensationDb( beamforming.maxCompensationDb ?? DEFAULT_MAX_COMPENSATION_DB, "beamforming.maxCompensationDb", ); const sourceCount = resolveSourceCountConfiguration( beamforming, microphoneCount, hopSize, fftSize, ); return { algorithm, sampleRate, arrayDefinition, scene, experiment, microphoneCount, pointCount, fftSize, hopSize, bandBins, bandHypotheses, analysisHypotheses, hypothesesByBin, selectedBins, minimumDistance, compensateFrequencyResponse, maxCompensationDb, sourceCount, }; } function scanCoordinate(minimum, maximum, index, count, reverse = false) { if (count === 1) return reverse ? maximum : minimum; const amount = index / (count - 1); return reverse ? maximum + (minimum - maximum) * amount : minimum + (maximum - minimum) * amount; } export function createMusicDistanceMatrix(arrayDefinition, scan) { const microphoneCount = arrayDefinition.microphones.length; const pointCount = checkedProduct( [scan.columns, scan.rows], "MUSIC scan point count", ); const elementCount = checkedProduct( [pointCount, microphoneCount], "MUSIC distance element count", ); const distances = new Float64Array(elementCount); let pointIndex = 0; for (let row = 0; row < scan.rows; row += 1) { const y = scanCoordinate(scan.yMin, scan.yMax, row, scan.rows, true); for (let column = 0; column < scan.columns; column += 1) { const x = scanCoordinate(scan.xMin, scan.xMax, column, scan.columns); const offset = pointIndex * microphoneCount; for ( let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1 ) { const position = arrayDefinition.microphones[microphoneIndex]?.position; if ( !Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite) ) { throw new TypeError( `arrayDefinition.microphones[${microphoneIndex}].position must be finite [x,y,z].`, ); } distances[offset + microphoneIndex] = Math.hypot( x - position[0], y - position[1], scan.z - position[2], ); } pointIndex += 1; } } return distances; } /** * Build exact diagonal preprocessing and the residual microphone response. * Sensitivity and phaseOffset are always removed. When response compensation * is enabled, response phase is removed exactly and only inverse gain boost is * capped; residual gain remains in the steering manifold. */ export function createMusicBinCalibration(options) { const { arrayDefinition, bin, sampleRate, fftSize, analogFrequencyHz: requestedAnalogFrequencyHz, signedAdcFrequencyHz: requestedSignedAdcFrequencyHz, spectralInversion = false, compensateFrequencyResponse = true, maxCompensationDb: maximumCompensationDb, } = options; const microphoneCount = arrayDefinition.microphones.length; const dataGains = new Float64Array(microphoneCount); const dataPhases = new Float64Array(microphoneCount); const manifoldGains = new Float64Array(microphoneCount); const manifoldPhases = new Float64Array(microphoneCount); const observedFrequencyHz = (bin * sampleRate) / fftSize; const analogFrequencyHz = requestedAnalogFrequencyHz ?? observedFrequencyHz; const signedAdcFrequencyHz = requestedSignedAdcFrequencyHz ?? (spectralInversion ? -observedFrequencyHz : observedFrequencyHz); requirePositiveFinite( analogFrequencyHz, "analogFrequencyHz", ); if ( !Number.isFinite(signedAdcFrequencyHz) || Math.abs( Math.abs(signedAdcFrequencyHz) - observedFrequencyHz, ) > 32 * Number.EPSILON * Math.max(1, observedFrequencyHz) ) { throw new RangeError( "signedAdcFrequencyHz must map to the selected positive ADC bin.", ); } if ((signedAdcFrequencyHz < 0) !== spectralInversion) { throw new RangeError( "spectralInversion must match signedAdcFrequencyHz.", ); } const orientationSign = spectralInversion ? -1 : 1; const maxCompensationDb = requireMaxCompensationDb( maximumCompensationDb ?? DEFAULT_MAX_COMPENSATION_DB, ); const response = {}; for ( let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1 ) { const microphone = arrayDefinition.microphones[microphoneIndex]; interpolateFrequencyResponse( microphone.frequencyResponse, analogFrequencyHz, response, ); const inverseGainDb = compensateFrequencyResponse ? inverseResponseGainDb(response.gainDb, maxCompensationDb) : 0; dataGains[microphoneIndex] = dbToLinear( -(microphone.sensitivityDb ?? 0) + inverseGainDb, ); dataPhases[microphoneIndex] = -orientationSign * ( ((microphone.phaseOffsetDeg ?? 0) * Math.PI) / 180 + (compensateFrequencyResponse ? response.phaseRadians : 0) ); manifoldGains[microphoneIndex] = dbToLinear( response.gainDb + inverseGainDb, ); manifoldPhases[microphoneIndex] = compensateFrequencyResponse ? 0 : orientationSign * response.phaseRadians; if ( !Number.isFinite(dataGains[microphoneIndex]) || !Number.isFinite(dataPhases[microphoneIndex]) || !Number.isFinite(manifoldGains[microphoneIndex]) || !Number.isFinite(manifoldPhases[microphoneIndex]) ) { throw new RangeError( `microphone ${microphoneIndex} calibration is not finite at ${analogFrequencyHz} Hz.`, ); } } return { bin, frequencyHz: analogFrequencyHz, analogFrequencyHz, observedFrequencyHz, signedAdcFrequencyHz, spectralInversion, orientationSign, dataGains, dataPhases, manifoldGains, manifoldPhases, model: Object.freeze({ sensitivity: "exact-inverse", phaseOffset: "exact-inverse", frequencyResponse: compensateFrequencyResponse ? "analog-hypothesis-inverse-phase-capped-inverse-gain" : "uncompensated-retained-in-manifold", frequencyResponseEvaluation: "configured-analog-fft-grid-hypothesis", spectralInversion: "positive-bin-full-complex-conjugation", compensateFrequencyResponse, maxCompensationDb, }), }; } export function applyMusicBinCalibration( spectrum, frameCount, microphoneCount, calibration, ) { const expectedLength = checkedProduct( [frameCount, microphoneCount], "MUSIC spectrum length", ); if ( !(spectrum?.real instanceof Float64Array) || !(spectrum?.imaginary instanceof Float64Array) || spectrum.real.length !== expectedLength || spectrum.imaginary.length !== expectedLength ) { throw new RangeError( "MUSIC spectrum must contain matching Float64 components.", ); } if ( calibration.dataGains.length !== microphoneCount || calibration.dataPhases.length !== microphoneCount ) { throw new RangeError( "MUSIC calibration length must match microphone count.", ); } for ( let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1 ) { const gain = calibration.dataGains[microphoneIndex]; const phase = calibration.dataPhases[microphoneIndex]; const correctionReal = gain * Math.cos(phase); const correctionImaginary = gain * Math.sin(phase); for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) { const index = frameIndex * microphoneCount + microphoneIndex; const inputReal = spectrum.real[index]; const inputImaginary = spectrum.imaginary[index]; spectrum.real[index] = inputReal * correctionReal - inputImaginary * correctionImaginary; spectrum.imaginary[index] = inputReal * correctionImaginary + inputImaginary * correctionReal; } } return spectrum; } function musicPlanByteLayout(configuration) { const { pointCount, microphoneCount, analysisHypotheses, fftSize, } = configuration; const distanceBytes = checkedProduct( [pointCount, microphoneCount, FLOAT64_BYTES], "MUSIC distance bytes", ); const stftWorkspaceBytes = checkedProduct( [fftSize, 3, FLOAT64_BYTES], "MUSIC STFT workspace bytes", ); const steeringWorkspaceBytes = checkedProduct( [microphoneCount, 2, FLOAT64_BYTES], "MUSIC steering workspace bytes", ); const mapWorkspaceBytes = checkedProduct( [pointCount, FLOAT64_BYTES], "MUSIC map workspace bytes", ); const calibrationBytes = checkedProduct( [ microphoneCount, analysisHypotheses.length, 4, FLOAT64_BYTES, ], "MUSIC calibration bytes", ); const retainedBytes = checkedSum( [ distanceBytes, stftWorkspaceBytes, steeringWorkspaceBytes, mapWorkspaceBytes, calibrationBytes, ], "MUSIC analysis-plan bytes", ); return { distanceBytes, stftWorkspaceBytes, steeringWorkspaceBytes, mapWorkspaceBytes, calibrationBytes, retainedBytes, }; } export function createMusicAnalysisPlan(options) { const configuration = resolveConfiguration(options); const maximumPlanBytes = options.maximumPlanBytes ?? DEFAULT_MAXIMUM_MUSIC_PLAN_BYTES; requirePositiveSafeInteger(maximumPlanBytes, "maximumPlanBytes"); const bytes = musicPlanByteLayout(configuration); if (bytes.retainedBytes > maximumPlanBytes) { throw new RangeError( `MUSIC analysis plan requires ${bytes.retainedBytes.toLocaleString()} bytes; ` + `maximumPlanBytes is ${maximumPlanBytes.toLocaleString()}.`, ); } const distances = createMusicDistanceMatrix( configuration.arrayDefinition, configuration.experiment.scan, ); const calibrations = new Map(); for (const hypothesis of configuration.analysisHypotheses) { calibrations.set( hypothesis.hypothesisKey, createMusicBinCalibration({ arrayDefinition: configuration.arrayDefinition, bin: hypothesis.bin, sampleRate: configuration.sampleRate, fftSize: configuration.fftSize, analogFrequencyHz: hypothesis.analogFrequencyHz, signedAdcFrequencyHz: hypothesis.signedAdcFrequencyHz, spectralInversion: hypothesis.spectralInversion, compensateFrequencyResponse: configuration.compensateFrequencyResponse, maxCompensationDb: configuration.maxCompensationDb, }), ); } return { kind: MUSIC_ANALYSIS_PLAN_KIND, ...configuration, maximumPlanBytes, ...bytes, distances, calibrations, stftWorkspace: createStftWorkspace(configuration.fftSize), steeringReal: new Float64Array(configuration.microphoneCount), steeringImaginary: new Float64Array(configuration.microphoneCount), mapWorkspace: new Float64Array(configuration.pointCount), busy: false, statistics: { analysisAttempts: 0, completedAnalyses: 0, distanceMatrixBuilds: 1, avoidedDistanceMatrixBuilds: 0, distanceEvaluations: configuration.pointCount * configuration.microphoneCount, calibrationBuilds: configuration.analysisHypotheses.length, calibrationCacheHits: 0, stftWorkspaceUses: 0, avoidedStftWorkspaceAllocations: 0, stftTransforms: 0, fftButterflies: 0, covarianceBuilds: 0, covarianceComplexAccumulations: 0, eigendecompositions: 0, jacobiSweeps: 0, jacobiRotations: 0, jacobiPairUpdates: 0, projectionComplexMacs: 0, mapWorkspaceUses: 0, retainedBytes: bytes.retainedBytes, }, }; } function assertCompatibleAnalysisPlan(plan, options) { if (plan?.kind !== MUSIC_ANALYSIS_PLAN_KIND) { throw new TypeError( "analysisPlan was not created by createMusicAnalysisPlan().", ); } if ( plan.sampleRate !== options.sampleRate || plan.arrayDefinition !== options.arrayDefinition || plan.scene !== options.scene || plan.experiment !== options.experiment ) { throw new Error( "analysisPlan must be used with the exact configuration objects that created it.", ); } } function requireChannels(channels, microphoneCount) { if (!Array.isArray(channels) || channels.length !== microphoneCount) { throw new RangeError( `channel count ${channels?.length ?? 0} does not match microphone count ${microphoneCount}.`, ); } const sampleCount = channels[0]?.length; if (!Number.isSafeInteger(sampleCount) || sampleCount < 1) { throw new RangeError( "MUSIC channels must contain at least one sample.", ); } for (let index = 0; index < channels.length; index += 1) { const channel = channels[index]; if ( !(channel instanceof Float32Array) && !(channel instanceof Float64Array) ) { throw new TypeError( `channels[${index}] must be a Float32Array or Float64Array.`, ); } if (channel.length !== sampleCount) { throw new RangeError( "all MUSIC channels must have the same sample count.", ); } } return sampleCount; } export function estimateMusicWorkload(options) { const configuration = options.analysisPlan ?? resolveConfiguration(options); if (options.analysisPlan) { assertCompatibleAnalysisPlan(options.analysisPlan, options); } const sampleCount = requireChannels( options.channels, configuration.microphoneCount, ); const frameCount = configuration.sourceCount.method === "fixed" ? getFrameCount( sampleCount, configuration.fftSize, configuration.hopSize, ) : getCompleteFrameCount( sampleCount, configuration.fftSize, configuration.hopSize, ); if ( configuration.sourceCount.method !== "fixed" && frameCount <= configuration.microphoneCount ) { throw new MusicUnavailableError( "insufficient-snapshots-for-source-count-selection", `MUSIC ${configuration.sourceCount.method.toUpperCase()} source-count selection ` + `requires frameCount > microphoneCount; got ${frameCount} <= ` + `${configuration.microphoneCount}. Non-overlap does not prove statistical ` + "independence; the selected Wax-Kailath model assumes it. No eigenvalue " + "floor or fallback is applied.", { frameCount, microphoneCount: configuration.microphoneCount, }, ); } const { pointCount, microphoneCount, } = configuration; const uniqueBinCount = configuration.selectedBins.length; const hypothesisCount = configuration.analysisHypotheses.length; const bandCount = configuration.experiment.frequencyBands.length; const planBytes = options.analysisPlan ? options.analysisPlan.retainedBytes : musicPlanByteLayout(configuration).retainedBytes; const resultMapBytes = checkedProduct( [bandCount, pointCount, 2, FLOAT64_BYTES], "MUSIC result-map bytes", ); const retainedHypothesisMapBytes = checkedProduct( [hypothesisCount, pointCount, FLOAT64_BYTES], "MUSIC retained per-hypothesis map bytes", ); const spectrumBytesPerBin = checkedProduct( [frameCount, microphoneCount, 2, FLOAT64_BYTES], "MUSIC Float64 spectrum bytes per bin", ); const calibratedSpectrumBytes = spectrumBytesPerBin; const covariance = estimateComplexCovarianceWorkload({ sensorCount: microphoneCount, snapshotCount: frameCount, }); const maximumJacobiSweeps = options.maximumJacobiSweeps ?? DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS; const eigen = estimateHermitianEigenWorkload({ order: microphoneCount, maximumSweeps: maximumJacobiSweeps, }); const covarianceAuditBytes = checkedProduct( [microphoneCount, 2, FLOAT64_BYTES], "MUSIC covariance audit bytes", ); const sourceCountSelectionBytes = configuration.sourceCount.method === "fixed" ? 0 : checkedProduct( [microphoneCount, FLOAT64_BYTES], "MUSIC source-count score bytes", ); const perBinNumericalBytes = checkedSum( [ covariance.allocationBytes, eigen.allocationBytes, covarianceAuditBytes, sourceCountSelectionBytes, ], "MUSIC per-bin numerical bytes", ); const baseWorkingBytes = checkedSum( [ planBytes, resultMapBytes, retainedHypothesisMapBytes, perBinNumericalBytes, calibratedSpectrumBytes, ], "MUSIC base working bytes", ); const covarianceComplexAccumulations = checkedProduct( [hypothesisCount, covariance.complexAccumulations], "MUSIC covariance complex accumulations", ); const noiseSubspaceDimension = configuration.sourceCount.method === "fixed" ? microphoneCount - configuration.sourceCount.value : microphoneCount; const projectionComplexMacs = checkedProduct( [ hypothesisCount, pointCount, microphoneCount, noiseSubspaceDimension, ], "MUSIC projection complex MAC count", ); const jacobiWorstCasePairUpdates = checkedProduct( [hypothesisCount, eigen.worstCasePairUpdates], "MUSIC Jacobi pair-update count", ); const stftButterfliesPerBatch = frameCount * microphoneCount * (configuration.fftSize / 2) * Math.log2(configuration.fftSize); if (!Number.isSafeInteger(stftButterfliesPerBatch)) { throw new RangeError( "MUSIC STFT butterfly count exceeds the safe integer range.", ); } return { algorithm: "music", rawScale: "noise-projection", objectiveDirection: "minimum", localizationModel: "near-field-spherical-direct-path-configured-analog-hypothesis", originalCarrierUsed: true, bandBins: configuration.bandBins, bandHypotheses: configuration.bandHypotheses, selectedBins: configuration.selectedBins, sampleCount, frameCount, microphoneCount, pointCount, bandCount, uniqueBinCount, hypothesisCount, planBytes, resultMapBytes, retainedBinMapBytes: retainedHypothesisMapBytes, retainedHypothesisMapBytes, spectrumBytesPerBin, calibratedSpectrumBytes, covariance, eigen, covarianceAuditBytes, sourceCountSelectionBytes, perBinNumericalBytes, baseWorkingBytes, covarianceComplexAccumulations, projectionComplexMacs, jacobiWorstCasePairUpdates, stftButterfliesPerBatch, sourceCountMethod: configuration.sourceCount.method, assumedNoiseSubspaceDimension: noiseSubspaceDimension, }; } function assertWorkLimits(workload, options) { const maximumWorkingBytes = options.maximumWorkingBytes ?? DEFAULT_MAXIMUM_MUSIC_WORKING_BYTES; const maximumCovarianceComplexAccumulations = options.maximumCovarianceComplexAccumulations ?? DEFAULT_MAXIMUM_MUSIC_COVARIANCE_COMPLEX_ACCUMULATIONS; const maximumProjectionComplexMacs = options.maximumProjectionComplexMacs ?? DEFAULT_MAXIMUM_MUSIC_PROJECTION_COMPLEX_MACS; const maximumJacobiPairUpdates = options.maximumJacobiPairUpdates ?? DEFAULT_MAXIMUM_MUSIC_JACOBI_PAIR_UPDATES; const maximumFftButterflies = options.maximumFftButterflies ?? DEFAULT_MAXIMUM_MUSIC_FFT_BUTTERFLIES; requirePositiveSafeInteger(maximumWorkingBytes, "maximumWorkingBytes"); requireNonNegativeFinite( maximumCovarianceComplexAccumulations, "maximumCovarianceComplexAccumulations", ); requireNonNegativeFinite( maximumProjectionComplexMacs, "maximumProjectionComplexMacs", ); requireNonNegativeFinite( maximumJacobiPairUpdates, "maximumJacobiPairUpdates", ); requireNonNegativeFinite( maximumFftButterflies, "maximumFftButterflies", ); if ( workload.covarianceComplexAccumulations > maximumCovarianceComplexAccumulations ) { throw new RangeError( `MUSIC covariance requires ${workload.covarianceComplexAccumulations.toLocaleString()} ` + `complex accumulations; limit is ${maximumCovarianceComplexAccumulations.toLocaleString()}.`, ); } if (workload.projectionComplexMacs > maximumProjectionComplexMacs) { throw new RangeError( `MUSIC projection requires at most ${workload.projectionComplexMacs.toLocaleString()} ` + `complex MACs; limit is ${maximumProjectionComplexMacs.toLocaleString()}.`, ); } if (workload.jacobiWorstCasePairUpdates > maximumJacobiPairUpdates) { throw new RangeError( `MUSIC eigendecomposition requires at most ` + `${workload.jacobiWorstCasePairUpdates.toLocaleString()} Jacobi pair updates; ` + `limit is ${maximumJacobiPairUpdates.toLocaleString()}.`, ); } const availableSpectrumBytes = maximumWorkingBytes - workload.baseWorkingBytes; const memoryBatchCapacity = Math.floor( availableSpectrumBytes / workload.spectrumBytesPerBin, ); if (memoryBatchCapacity < 1) { const minimumBytes = workload.baseWorkingBytes + workload.spectrumBytesPerBin; throw new RangeError( `MUSIC requires at least ${minimumBytes.toLocaleString()} typed-array bytes; ` + `maximumWorkingBytes is ${maximumWorkingBytes.toLocaleString()}.`, ); } const requestedBatchLimit = options.maximumSpectrumBatchBins ?? Number.MAX_SAFE_INTEGER; requirePositiveSafeInteger( requestedBatchLimit, "maximumSpectrumBatchBins", ); const batchSize = Math.min( workload.uniqueBinCount, memoryBatchCapacity, requestedBatchLimit, ); const spectrumBatchCount = Math.ceil( workload.uniqueBinCount / batchSize, ); const fftButterflyCount = checkedProduct( [spectrumBatchCount, workload.stftButterfliesPerBatch], "MUSIC FFT butterfly count", ); if (fftButterflyCount > maximumFftButterflies) { throw new RangeError( `MUSIC STFT requires ${fftButterflyCount.toLocaleString()} butterflies; ` + `limit is ${maximumFftButterflies.toLocaleString()}.`, ); } return { maximumWorkingBytes, maximumCovarianceComplexAccumulations, maximumProjectionComplexMacs, maximumJacobiPairUpdates, maximumFftButterflies, memoryBatchCapacity, batchSize, spectrumBatchCount, fftButterflyCount, estimatedPeakWorkingBytes: workload.baseWorkingBytes + batchSize * workload.spectrumBytesPerBin, }; } function sourceCountFromEigensystem(configuration, audit, frameCount) { if (configuration.sourceCount.method === "fixed") { return { method: "fixed", status: "available", value: configuration.sourceCount.value, criterion: null, score: null, scores: null, assumptions: { userSpecified: true, independentSnapshotsRequired: false, }, }; } const selection = selectWaxKailathSourceCount({ eigenvalues: audit.roundoffClampedEigenvalues, snapshotCount: frameCount, independentSnapshots: true, criterion: configuration.sourceCount.method, roundoffTolerance: audit.roundoffTolerance, }); if (!selection.available) { throw new MusicUnavailableError( selection.reason, `MUSIC ${configuration.sourceCount.method.toUpperCase()} source-count selection ` + `is unavailable: ${selection.reason}. No fallback or eigenvalue floor is applied.`, { sourceCountMethod: configuration.sourceCount.method, snapshotCount: frameCount, }, ); } if (selection.sourceCount === 0) { throw new MusicUnavailableError( "model-order-zero-no-source", `MUSIC ${configuration.sourceCount.method.toUpperCase()} selected zero sources; ` + "a source location is undefined, so no arbitrary minimum is returned.", { sourceCountMethod: configuration.sourceCount.method, snapshotCount: frameCount, }, ); } return { method: selection.criterion, status: "available", value: selection.sourceCount, criterion: selection.criterion, score: selection.score, scores: Array.from(selection.scores), assumptions: { userSpecified: false, independentSnapshotsRequired: true, nonOverlappingSnapshots: true, partialFrames: "excluded", statisticalSnapshotIndependence: { assumed: true, evidence: "non-overlapping-frames-only-not-statistical-proof", modelSelectedByUser: true, }, statisticalDataModel: { assumed: true, verified: false, assumptions: [ "iid-complex-gaussian-snapshots", "spatially-white-noise", "stationary-source-count-over-snapshots", ], interpretation: "wax-kailath-model-order-criterion-not-probability-or-confidence", }, snapshotCount: selection.snapshotCount, sensorCount: selection.sensorCount, roundoffTolerance: selection.roundoffTolerance, logRatioRoundoffClampCount: selection.logRatioRoundoffClampCount, }, }; } function requireIdentifiablePartition( eigenvalues, sourceCount, audit, eigensystem, ) { const signalEdge = eigenvalues[sourceCount - 1]; const noiseEdge = eigenvalues[sourceCount]; const partitionTolerance = Math.max( audit.roundoffTolerance, eigensystem.residualFrobeniusNorm, ); const eigenvalueGap = signalEdge - noiseEdge; if (!(eigenvalueGap > partitionTolerance)) { throw new MusicUnavailableError( "signal-noise-partition-unidentifiable", `MUSIC signal/noise subspace is not identifiable for source count ${sourceCount}: ` + `eigenvalue gap ${eigenvalueGap} is not above numerical audit bound ` + `${partitionTolerance}. Coherent or rank-deficient sources are not repaired ` + "with diagonal loading or spatial smoothing.", { sourceCount, eigenvalueGap, partitionTolerance, }, ); } return { signalEdge, noiseEdge, eigenvalueGap, partitionTolerance, }; } export async function computeMusicNoiseProjectionMap(options) { const { distances, pointCount, microphoneCount, calibration, eigenvectors, sourceCount, speedOfSound, minimumDistance, output = new Float64Array(pointCount), steeringReal = new Float64Array(microphoneCount), steeringImaginary = new Float64Array(microphoneCount), pointBatchSize = 64, signal, } = options; const steeringFrequencyHz = options.steeringFrequencyHz ?? options.observedFrequencyHz; requirePositiveSafeInteger(pointBatchSize, "pointBatchSize"); if ( !(distances instanceof Float64Array) || distances.length !== pointCount * microphoneCount ) { throw new RangeError( "MUSIC distances must be a point-major Float64 matrix.", ); } if ( !(output instanceof Float64Array) || output.length !== pointCount || !(steeringReal instanceof Float64Array) || steeringReal.length !== microphoneCount || !(steeringImaginary instanceof Float64Array) || steeringImaginary.length !== microphoneCount ) { throw new RangeError( "MUSIC output and steering workspaces must be Float64 and correctly sized.", ); } if ( eigenvectors?.layout !== "row-major-columns" || eigenvectors.real?.length !== microphoneCount * microphoneCount || eigenvectors.imaginary?.length !== microphoneCount * microphoneCount ) { throw new RangeError( "MUSIC eigenvectors must use row-major-columns layout.", ); } if ( calibration?.manifoldGains?.length !== microphoneCount || calibration?.manifoldPhases?.length !== microphoneCount ) { throw new RangeError( "MUSIC manifold calibration must match microphone count.", ); } if ( !Number.isFinite(steeringFrequencyHz) || steeringFrequencyHz === 0 ) { throw new RangeError( "steeringFrequencyHz must be finite and non-zero.", ); } requirePositiveFinite(speedOfSound, "speedOfSound"); requirePositiveFinite(minimumDistance, "minimumDistance"); if ( !Number.isSafeInteger(sourceCount) || sourceCount < 1 || sourceCount >= microphoneCount ) { throw new RangeError( "sourceCount must be inside 1..microphoneCount-1.", ); } const angularWaveNumber = (TAU * steeringFrequencyHz) / speedOfSound; for ( let pointIndex = 0; pointIndex < pointCount; pointIndex += 1 ) { throwIfAborted(signal); const distanceOffset = pointIndex * microphoneCount; const referenceDistance = distances[distanceOffset]; let steeringNormSquared = 0; for ( let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1 ) { const distance = distances[distanceOffset + microphoneIndex]; const amplitude = calibration.manifoldGains[microphoneIndex] / Math.max(distance, minimumDistance); const phase = -angularWaveNumber * (distance - referenceDistance) + calibration.manifoldPhases[microphoneIndex]; const valueReal = amplitude * Math.cos(phase); const valueImaginary = amplitude * Math.sin(phase); steeringReal[microphoneIndex] = valueReal; steeringImaginary[microphoneIndex] = valueImaginary; steeringNormSquared += valueReal * valueReal + valueImaginary * valueImaginary; } if ( !(steeringNormSquared > 0) || !Number.isFinite(steeringNormSquared) ) { throw new RangeError( `MUSIC steering norm is not finite and positive at scan point ${pointIndex}.`, ); } let noiseProjection = 0; for ( let column = sourceCount; column < microphoneCount; column += 1 ) { let dotReal = 0; let dotImaginary = 0; for ( let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1 ) { const vectorIndex = microphoneIndex * microphoneCount + column; const vectorReal = eigenvectors.real[vectorIndex]; const vectorImaginary = eigenvectors.imaginary[vectorIndex]; const valueReal = steeringReal[microphoneIndex]; const valueImaginary = steeringImaginary[microphoneIndex]; dotReal += vectorReal * valueReal + vectorImaginary * valueImaginary; dotImaginary += vectorReal * valueImaginary - vectorImaginary * valueReal; } noiseProjection += dotReal * dotReal + dotImaginary * dotImaginary; } const quotient = noiseProjection / steeringNormSquared; if (!Number.isFinite(quotient) || quotient < 0) { throw new RangeError( `MUSIC noise projection is not finite and non-negative at scan point ${pointIndex}.`, ); } output[pointIndex] = quotient; if ( (pointIndex + 1) % pointBatchSize === 0 && pointIndex + 1 < pointCount ) { await cooperativeYield(); throwIfAborted(signal); } } return output; } function findMinimum(values) { if (values.length === 0) { throw new RangeError( "cannot find a MUSIC minimum in an empty map.", ); } let index = 0; let value = values[0]; let maximum = values[0]; for (let candidate = 1; candidate < values.length; candidate += 1) { maximum = Math.max(maximum, values[candidate]); if (values[candidate] < value) { index = candidate; value = values[candidate]; } } return { index, value, flat: value === maximum, }; } function plainEigensolverDiagnostics(eigensystem) { return { solver: "full-cyclic-complex-hermitian-jacobi", converged: eigensystem.converged, sweeps: eigensystem.sweeps, rotations: eigensystem.rotations, scale: eigensystem.scale, inputHermitianError: eigensystem.inputHermitianError, inputHermitianTolerance: eigensystem.inputHermitianTolerance, relativeTolerance: eigensystem.relativeTolerance, offDiagonalFrobeniusNorm: eigensystem.offDiagonalFrobeniusNorm, relativeOffDiagonal: eigensystem.relativeOffDiagonal, residualFrobeniusNorm: eigensystem.residualFrobeniusNorm, relativeResidual: eigensystem.relativeResidual, residualTolerance: eigensystem.residualTolerance, orthogonalityError: eigensystem.orthogonalityError, orthogonalityTolerance: eigensystem.orthogonalityTolerance, numericallyVerified: eigensystem.numericallyVerified, }; } function calibrationFor(configuration, plan, hypothesis) { if (plan) { plan.statistics.calibrationCacheHits += 1; return plan.calibrations.get( hypothesis.hypothesisKey, ); } return createMusicBinCalibration({ arrayDefinition: configuration.arrayDefinition, bin: hypothesis.bin, sampleRate: configuration.sampleRate, fftSize: configuration.fftSize, analogFrequencyHz: hypothesis.analogFrequencyHz, signedAdcFrequencyHz: hypothesis.signedAdcFrequencyHz, spectralInversion: hypothesis.spectralInversion, compensateFrequencyResponse: configuration.compensateFrequencyResponse, maxCompensationDb: configuration.maxCompensationDb, }); } function analysisCacheSummary(plan) { const statistics = plan.statistics; return { analysisAttempts: statistics.analysisAttempts, completedAnalyses: statistics.completedAnalyses, distanceMatrixBuilds: statistics.distanceMatrixBuilds, avoidedDistanceMatrixBuilds: statistics.avoidedDistanceMatrixBuilds, distanceEvaluations: statistics.distanceEvaluations, calibrationBuilds: statistics.calibrationBuilds, calibrationCacheHits: statistics.calibrationCacheHits, stftWorkspaceUses: statistics.stftWorkspaceUses, avoidedStftWorkspaceAllocations: statistics.avoidedStftWorkspaceAllocations, stftTransforms: statistics.stftTransforms, fftButterflies: statistics.fftButterflies, covarianceBuilds: statistics.covarianceBuilds, covarianceComplexAccumulations: statistics.covarianceComplexAccumulations, eigendecompositions: statistics.eigendecompositions, jacobiSweeps: statistics.jacobiSweeps, jacobiRotations: statistics.jacobiRotations, jacobiPairUpdates: statistics.jacobiPairUpdates, projectionComplexMacs: statistics.projectionComplexMacs, mapWorkspaceUses: statistics.mapWorkspaceUses, retainedBytes: statistics.retainedBytes, maximumPlanBytes: plan.maximumPlanBytes, }; } function finalizeMusicBands( hypothesisResults, configuration, ) { return configuration.experiment.frequencyBands.map( (band, bandIndex) => { const hypothesis = configuration.bandHypotheses[bandIndex]; const bin = hypothesis.bin; const result = hypothesisResults.get( hypothesis.hypothesisKey, ); const rawMap = Float64Array.from(result.map); const displayMap = Float64Array.from(rawMap); const minimum = findMinimum(rawMap); return { id: band.id, color: band.color, bins: [bin], observedFrequencyHz: result.observedFrequencyHz, analogFrequencyHz: hypothesis.analogFrequencyHz, signedAdcFrequencyHz: hypothesis.signedAdcFrequencyHz, spectralInversion: hypothesis.spectralInversion, aliasOrder: hypothesis.aliasOrder, frequencyHypothesisCount: 1, sharedObservationKey: hypothesis.sharedObservationKey, localizationModel: "near-field-spherical-direct-path-configured-analog-hypothesis", originalCarrierUsed: true, requestedBandAliased: hypothesis.aliased, aliasOrderResolution: hypothesis.aliased ? "configured-single-hypothesis" : "not-applicable", rawMap, rawScale: "noise-projection", displayMap, displayScale: "noise-projection", displayTransform: "identity", objectiveDirection: "minimum", identifiability: minimum.flat ? "spatially-unidentifiable-flat-objective" : "non-flat-objective", peak: minimum.flat ? null : { index: minimum.index, position: scanIndexToPosition( minimum.index, configuration.experiment.scan, ), rawValue: minimum.value, rawNoiseProjection: minimum.value, rawScale: "noise-projection", objectiveDirection: "minimum", }, sourceCount: result.sourceCount, diagnostics: result.diagnostics, }; }, ); } async function computeMusicMapInternal(options, plan) { const configuration = plan ?? resolveConfiguration(options); const { channels, signal, onProgress, pointBatchSize = 64, stftTransformBatchSize = 32, } = options; requirePositiveSafeInteger(pointBatchSize, "pointBatchSize"); requirePositiveSafeInteger( stftTransformBatchSize, "stftTransformBatchSize", ); const workload = estimateMusicWorkload({ ...options, ...(plan ? { analysisPlan: plan } : {}), }); const limits = assertWorkLimits(workload, options); const distances = plan?.distances ?? createMusicDistanceMatrix( configuration.arrayDefinition, configuration.experiment.scan, ); const steeringReal = plan?.steeringReal ?? new Float64Array(configuration.microphoneCount); const steeringImaginary = plan?.steeringImaginary ?? new Float64Array(configuration.microphoneCount); const mapWorkspace = plan?.mapWorkspace ?? new Float64Array(configuration.pointCount); const hypothesisResults = new Map(); let completedHypotheses = 0; for ( let batchStart = 0; batchStart < configuration.selectedBins.length; batchStart += limits.batchSize ) { throwIfAborted(signal); const batchBins = configuration.selectedBins.slice( batchStart, batchStart + limits.batchSize, ); if (plan) { plan.statistics.stftWorkspaceUses += 1; plan.statistics.avoidedStftWorkspaceAllocations = Math.max( 0, plan.statistics.stftWorkspaceUses - 1, ); } const stft = await extractStftBins(channels, { fftSize: configuration.fftSize, hopSize: configuration.hopSize, bins: batchBins, signal, transformBatchSize: stftTransformBatchSize, workspace: plan?.stftWorkspace, spectrumPrecision: "float64", includePartialFrame: configuration.sourceCount.method === "fixed", onProgress: ({ completedTransforms, totalTransforms }) => onProgress?.({ stage: "stft", completedBins: completedHypotheses, totalBins: configuration.analysisHypotheses.length, completedTransforms, totalTransforms, spectrumBatch: Math.floor(batchStart / limits.batchSize) + 1, spectrumBatchCount: limits.spectrumBatchCount, }), }); if (plan) { const transformCount = stft.frameCount * stft.microphoneCount; plan.statistics.stftTransforms += transformCount; plan.statistics.fftButterflies += transformCount * (configuration.fftSize / 2) * Math.log2(configuration.fftSize); } await cooperativeYield(); throwIfAborted(signal); for (const bin of batchBins) { throwIfAborted(signal); const rawSpectrum = stft.spectra.get(bin); const hypotheses = configuration.hypothesesByBin.get(bin) ?? []; for (const hypothesis of hypotheses) { throwIfAborted(signal); const calibration = calibrationFor( configuration, plan, hypothesis, ); const spectrum = applyMusicBinCalibration( { real: Float64Array.from(rawSpectrum.real), imaginary: Float64Array.from( rawSpectrum.imaginary, ), }, stft.frameCount, stft.microphoneCount, calibration, ); onProgress?.({ stage: "music-covariance", completedBins: completedHypotheses, totalBins: configuration.analysisHypotheses.length, }); const covariance = computeComplexSampleCovariance({ real: spectrum.real, imaginary: spectrum.imaginary, sensorCount: configuration.microphoneCount, snapshotCount: stft.frameCount, layout: "snapshot-major", maximumWorkBytes: limits.maximumWorkingBytes, maximumComplexAccumulations: limits.maximumCovarianceComplexAccumulations, }); if (plan) { plan.statistics.covarianceBuilds += 1; plan.statistics.covarianceComplexAccumulations += covariance.workload.complexAccumulations; } throwIfAborted(signal); onProgress?.({ stage: "music-eigendecomposition", completedBins: completedHypotheses, totalBins: configuration.analysisHypotheses.length, }); const eigensystem = decomposeHermitian({ real: covariance.matrix.real, imaginary: covariance.matrix.imaginary, order: configuration.microphoneCount, maximumSweeps: options.maximumJacobiSweeps ?? DEFAULT_HERMITIAN_JACOBI_MAX_SWEEPS, maximumWorkBytes: limits.maximumWorkingBytes, maximumPairUpdates: limits.maximumJacobiPairUpdates, }); if (!eigensystem.converged) { throw new RangeError( "MUSIC Hermitian eigendecomposition did not converge after " + `${eigensystem.sweeps} sweeps; no partial subspace is returned.`, ); } if (!eigensystem.numericallyVerified) { throw new RangeError( "MUSIC Hermitian eigendecomposition failed residual or orthogonality " + "verification; no unverified subspace is returned.", ); } if (plan) { plan.statistics.eigendecompositions += 1; plan.statistics.jacobiSweeps += eigensystem.sweeps; plan.statistics.jacobiRotations += eigensystem.rotations; plan.statistics.jacobiPairUpdates += eigensystem.rotations * eigensystem.workload.pairUpdatesPerRotation; } const audit = auditCovarianceEigenvalues({ eigenvalues: eigensystem.eigenvalues, matrixScale: covariance.matrixScale, sensorCount: configuration.microphoneCount, covarianceRoundoffBound: covariance.covarianceRoundoffBound, eigensolverResidualFrobeniusNorm: eigensystem.residualFrobeniusNorm, }); const sourceCount = sourceCountFromEigensystem( configuration, audit, stft.frameCount, ); const partition = requireIdentifiablePartition( audit.roundoffClampedEigenvalues, sourceCount.value, audit, eigensystem, ); const map = await computeMusicNoiseProjectionMap({ distances, pointCount: configuration.pointCount, microphoneCount: configuration.microphoneCount, calibration, eigenvectors: eigensystem.eigenvectors, sourceCount: sourceCount.value, steeringFrequencyHz: hypothesis.orientationSign * hypothesis.analogFrequencyHz, speedOfSound: configuration.scene.speedOfSound, minimumDistance: configuration.minimumDistance, output: mapWorkspace, steeringReal, steeringImaginary, pointBatchSize, signal, }); const projectionComplexMacs = configuration.pointCount * configuration.microphoneCount * ( configuration.microphoneCount - sourceCount.value ); if (plan) { plan.statistics.projectionComplexMacs += projectionComplexMacs; plan.statistics.mapWorkspaceUses += 1; } hypothesisResults.set(hypothesis.hypothesisKey, { observedFrequencyHz: hypothesis.observedFrequencyHz, map: Float64Array.from(map), sourceCount, diagnostics: { frequencyHypothesis: { model: "configured-single-analog-fft-grid-hypothesis", hypothesisCount: 1, analogFrequencyHz: hypothesis.analogFrequencyHz, observedFrequencyHz: hypothesis.observedFrequencyHz, signedAdcFrequencyHz: hypothesis.signedAdcFrequencyHz, spectralInversion: hypothesis.spectralInversion, aliasOrder: hypothesis.aliasOrder, sharedObservationKey: hypothesis.sharedObservationKey, }, snapshotCount: stft.frameCount, covariance: { model: "sample-xxh-no-mean", normalization: covariance.normalization, centered: covariance.centered, inputLayout: covariance.inputLayout, matrixScale: covariance.matrixScale, trace: covariance.trace, covarianceRoundoffBound: covariance.covarianceRoundoffBound, }, eigenvalues: Array.from( eigensystem.eigenvalues, ), auditedEigenvalues: Array.from( audit.roundoffClampedEigenvalues, ), covarianceAudit: { roundoffTolerance: audit.roundoffTolerance, roundoffFactor: audit.roundoffFactor, eigenvalueScale: audit.eigenvalueScale, minimumEigenvalue: audit.minimumEigenvalue, isPositiveSemidefinite: audit.isPositiveSemidefinite, clampedNegativeCount: audit.clampedNegativeCount, roundoffDerivation: audit.roundoffDerivation, covarianceRoundoffBound: audit.covarianceRoundoffBound, eigensolverResidualFrobeniusNorm: audit.eigensolverResidualFrobeniusNorm, }, eigensolver: plainEigensolverDiagnostics(eigensystem), partition, sourceCount, projectionComplexMacs, }, }); completedHypotheses += 1; onProgress?.({ stage: "localization", completedBins: completedHypotheses, totalBins: configuration.analysisHypotheses.length, }); await cooperativeYield(); } } } const bands = finalizeMusicBands( hypothesisResults, configuration, ); return { algorithm: "music", backend: "cpu", frameCount: workload.frameCount, microphoneCount: workload.microphoneCount, pointCount: workload.pointCount, workload: { ...workload, ...limits }, fftButterflyCount: limits.fftButterflyCount, objectiveDirection: "minimum", rawScale: "noise-projection", model: { publicationFamily: "Schmidt narrowband MUSIC", covariance: "sample-xxh-no-mean", manifold: "near-field-spherical-direct-path-configured-analog-hypothesis", propagationPaths: "direct-only", localizationFrequency: "configured-single-analog-fft-grid-hypothesis-with-adc-inversion", hypothesisSource: "experiment-band-not-scene-truth", originalCarrierUsed: true, aliasOrderResolution: "configured-single-hypothesis-no-selection", multibandFusion: "none-each-band-is-separate-single-analog-hypothesis", spatialSmoothing: "none", diagonalLoading: "none", eigenvalueFloor: "none", normalizationApplied: "none-raw-noise-projection-preserved", requestedNormalization: configuration.experiment.beamforming.normalization, distanceAmplitude: "inverse-distance-with-explicit-minimum-distance", minimumDistance: configuration.minimumDistance, microphoneCalibration: { sensitivity: "exact-inverse", phaseOffset: "exact-inverse", frequencyResponse: configuration.compensateFrequencyResponse ? "analog-hypothesis-inverse-phase-capped-inverse-gain-residual-in-manifold" : "uncompensated-retained-in-manifold", frequencyResponseEvaluation: "configured-analog-fft-grid-hypothesis", spectralInversion: "positive-bin-full-complex-conjugation", compensateFrequencyResponse: configuration.compensateFrequencyResponse, maxCompensationDb: configuration.maxCompensationDb, }, }, bands, ...(plan ? { analysisCache: analysisCacheSummary(plan) } : {}), }; } export async function computeMusicMap(options) { const plan = options.analysisPlan; if (!plan) return computeMusicMapInternal(options, undefined); assertCompatibleAnalysisPlan(plan, options); if (plan.busy) { throw new Error( "analysisPlan is already in use by another MUSIC analysis.", ); } plan.busy = true; plan.statistics.analysisAttempts += 1; if (plan.statistics.analysisAttempts > 1) { plan.statistics.avoidedDistanceMatrixBuilds += 1; } try { const result = await computeMusicMapInternal(options, plan); plan.statistics.completedAnalyses += 1; result.analysisCache = analysisCacheSummary(plan); return result; } finally { plan.busy = false; } }