import { computeReceiveBeamPattern, computeReceiveBeamPatternSphere, receiveBeamPatternDirection, receiveBeamPatternSphereDirection, } from "/packages/acoustics-core/beam-pattern.js"; function assertNear(actual, expected, tolerance, label) { assert( Math.abs(actual - expected) <= tolerance, label + ": expected " + expected + " ± " + tolerance + ", received " + actual, ); } function arrayFromPositions(positions) { return { microphones: positions.map((position, index) => ({ id: "mic-" + String(index + 1), position, })), }; } function sampleAtAngle(pattern, angleDeg) { let bestIndex = 0; let bestDistance = Infinity; for (let index = 0; index < pattern.anglesDeg.length; index += 1) { const delta = Math.abs(pattern.anglesDeg[index] - angleDeg); const circularDistance = Math.min(delta, 360 - delta); if (circularDistance < bestDistance) { bestDistance = circularDistance; bestIndex = index; } } assert(bestDistance < 1e-10, "requested angle is present in sampled cut"); return pattern.responseLinear[bestIndex]; } test("receive beam-pattern cuts use the documented right-handed axes", () => { const cases = [ ["xy", 0, [1, 0, 0]], ["xy", 90, [0, 1, 0]], ["xz", 0, [0, 0, 1]], ["xz", 90, [1, 0, 0]], ["yz", 0, [0, 0, 1]], ["yz", 90, [0, 1, 0]], ]; for (const [cut, angleDeg, expected] of cases) { const actual = receiveBeamPatternDirection(cut, angleDeg); actual.forEach((value, axis) => assertNear(value, expected[axis], 1e-12, cut + " direction axis " + axis)); } }); test("one ideal point microphone is omnidirectional on every sampled cut", () => { const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([[12.5, -7, 3]]), frequencyHz: 49_125, speedOfSound: 343, cut: "yz", steeringAngleDeg: -35, sampleCount: 360, }); for (const response of pattern.responseLinear) { assertNear(response, 1, 1e-12, "single-microphone response"); } assertEqual( pattern.metrics.halfPowerBeamwidth.status, "full-circle-above-half-power", "flat cut status", ); assertEqual(pattern.metrics.halfPowerBeamwidth.widthDeg, null, "flat cut has no HPBW"); assert( pattern.metrics.halfPowerBeamwidth.fullCircleAboveHalfPower, "flat cut records the missing crossing", ); assertEqual( pattern.metrics.sampledLocalMaxima.peaks.length, 0, "flat cut has no unique peaks", ); }); test("half-wavelength two-microphone broadside cut has nulls, 60-degree HPBW, and front-back tie", () => { const speedOfSound = 343; const frequencyHz = 1_000; const spacing = speedOfSound / frequencyHz / 2; const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([[-spacing / 2, 0, 0], [spacing / 2, 0, 0]]), frequencyHz, speedOfSound, cut: "xz", steeringAngleDeg: 0, sampleCount: 1_440, }); assertNear(sampleAtAngle(pattern, 90), 0, 1e-12, "endfire null at +x"); assertNear(sampleAtAngle(pattern, 270), 0, 1e-12, "endfire null at -x"); assertNear(sampleAtAngle(pattern, 0), 1, 1e-12, "steered broadside response"); assertNear(sampleAtAngle(pattern, 180), 1, 1e-12, "planar front-back response"); assertEqual( pattern.metrics.halfPowerBeamwidth.status, "sampled-bracketed-bisection", "HPBW status", ); assertNear(pattern.metrics.halfPowerBeamwidth.widthDeg, 60, 1e-10, "power HPBW"); const zeroDbAngles = pattern.metrics.sampledLocalMaxima.peaks .filter((peak) => Math.abs(peak.responseDb) < 1e-10) .map((peak) => peak.angleDeg) .sort((left, right) => left - right); assertEqual(zeroDbAngles.join(","), "0,180", "front-back maxima remain tied"); assertEqual( pattern.metrics.sampledLocalMaxima.status, "tied-global-maxima-on-grid", "global tie is explicit", ); assertEqual(pattern.metrics.sampledLocalMaxima.selected, null, "tie has no selected peak"); assertEqual( pattern.metrics.sampledLocalMaxima.globalMaximumRegions.length, 2, "both tied regions are preserved", ); assertNear(pattern.responseReal[Math.floor(pattern.sampleCount / 2)], 1, 1e-12, "raw real"); assertNear(pattern.responseImaginary[Math.floor(pattern.sampleCount / 2)], 0, 1e-12, "raw imaginary"); assertNear(pattern.responsePower[Math.floor(pattern.sampleCount / 2)], 1, 1e-12, "raw power"); }); test("asymmetric array preserves the receive steering sign in raw complex response", () => { const speedOfSound = 343; const frequencyHz = 1_000; const quarterWavelength = speedOfSound / frequencyHz / 4; const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([ [0, 0, 0], [quarterWavelength, 0, 0], [3 * quarterWavelength, 0, 0], ]), frequencyHz, speedOfSound, cut: "xz", steeringAngleDeg: 0, sampleCount: 360, }); const lookIndex = [...pattern.anglesDeg].findIndex((angle) => angle === 90); assert(lookIndex >= 0, "+x look direction is sampled"); assertNear(pattern.responseReal[lookIndex], -1 / 6, 1e-12, "signed real response"); assertNear( pattern.responseImaginary[lookIndex], -Math.sqrt(3) / 6, 1e-12, "signed imaginary response", ); }); test("display dB floor never changes raw array factor or physical metrics", () => { const array = arrayFromPositions([[-0.08575, 0, 0], [0.08575, 0, 0]]); const shared = { array, frequencyHz: 1_000, speedOfSound: 343, cut: "xz", steeringAngleDeg: 0, sampleCount: 720, }; const shallow = computeReceiveBeamPattern({ ...shared, dbFloor: -30 }); const deep = computeReceiveBeamPattern({ ...shared, dbFloor: -100 }); for (let index = 0; index < shallow.sampleCount; index += 1) { assertNear( shallow.responsePower[index], deep.responsePower[index], 0, "raw power is floor-independent at " + index, ); } assertNear( shallow.metrics.halfPowerBeamwidth.widthDeg, deep.metrics.halfPowerBeamwidth.widthDeg, 0, "HPBW is floor-independent", ); const nullIndex = [...shallow.anglesDeg].findIndex((angle) => angle === 90); assertEqual(shallow.displayDb[nullIndex], -30, "shallow display floor"); assertEqual(deep.displayDb[nullIndex], -100, "deep display floor"); assert( shallow.responseDb[nullIndex] < -300, "raw null remains below both display floors", ); }); test("receive array factor is invariant to translating every microphone", () => { const basePositions = [[-0.12, 0.03, 0], [0.04, -0.08, 0.02], [0.15, 0.07, -0.01]]; const translation = [1_000, -2_000, 500]; const translatedPositions = basePositions.map((position) => position.map((coordinate, axis) => coordinate + translation[axis])); const options = { frequencyHz: 7_500, speedOfSound: 343, cut: "xy", steeringAngleDeg: 23, sampleCount: 720, }; const base = computeReceiveBeamPattern({ ...options, array: arrayFromPositions(basePositions) }); const translated = computeReceiveBeamPattern({ ...options, array: arrayFromPositions(translatedPositions), }); for (let index = 0; index < base.sampleCount; index += 1) { assertNear( translated.responseLinear[index], base.responseLinear[index], 1e-11, "translation-invariant sample " + index, ); } }); test("beam pattern uses analog frequency directly without Nyquist folding", () => { const highFrequencyHz = 49_125; const speedOfSound = 343; const halfHighWavelength = speedOfSound / (2 * highFrequencyHz); const array = arrayFromPositions([[0, 0, 0], [halfHighWavelength, 0, 0]]); const highAnalog = computeReceiveBeamPattern({ array, frequencyHz: highFrequencyHz, speedOfSound, cut: "xy", steeringAngleDeg: 90, sampleCount: 360, }); const foldedTemporalAlias = computeReceiveBeamPattern({ array, frequencyHz: 1_125, speedOfSound, cut: "xy", steeringAngleDeg: 90, sampleCount: 360, }); assertEqual(highAnalog.frequencyHz, highFrequencyHz, "analog hypothesis is preserved"); assertNear( highAnalog.wavelengthM, speedOfSound / highFrequencyHz, 1e-15, "analog wavelength", ); assert( Math.abs(sampleAtAngle(highAnalog, 0) - sampleAtAngle(foldedTemporalAlias, 0)) > 0.1, "spatial pattern is not computed from the folded ADC alias", ); }); test("coarse high-frequency cuts preserve raw samples but withhold unresolved metrics", () => { const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([[-0.5, 0, 0], [0.5, 0, 0]]), frequencyHz: 100_000, speedOfSound: 343, cut: "xz", steeringAngleDeg: 0, sampleCount: 360, }); assert(!pattern.angularSampling.metricResolutionSatisfied, "phase-step audit fails"); assert( pattern.angularSampling.maximumAdjacentPhaseStepRadBound > pattern.angularSampling.metricPhaseStepLimitRad, "result exposes the physical phase-step bound", ); assertEqual( pattern.metrics.halfPowerBeamwidth.status, "resolution-unresolved", "HPBW is withheld", ); assertEqual( pattern.metrics.sampledLocalMaxima.status, "resolution-unresolved", "peak interpretation is withheld", ); assertEqual(pattern.responsePower.length, 360, "raw sampled power remains available"); }); test("non-constant cuts without sampled half-power crossings do not claim 360-degree HPBW", () => { const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([[-0.0005, 0, 0], [0.0005, 0, 0]]), frequencyHz: 1_000, speedOfSound: 343, cut: "xz", steeringAngleDeg: 0, sampleCount: 720, }); assert(pattern.angularSampling.projectedApertureM > 0, "array is not constant on cut"); assert( Math.min(...pattern.responsePower) > 0.5, "sampled cut stays above half power", ); assertEqual( pattern.metrics.halfPowerBeamwidth.status, "no-sampled-half-power-crossing", "missing crossing remains unresolved", ); assertEqual(pattern.metrics.halfPowerBeamwidth.widthDeg, null, "no fabricated width"); assertEqual( pattern.metrics.halfPowerBeamwidth.fullCircleAboveHalfPower, null, "full-circle claim is withheld", ); }); test("auto angular sampling increases resolution and reports when the safety cap is insufficient", () => { const moderate = computeReceiveBeamPattern({ array: arrayFromPositions([[-0.225, 0, 0], [0.225, 0, 0]]), frequencyHz: 49_125, speedOfSound: 343, cut: "xz", steeringAngleDeg: 0, sampleCount: "auto", }); assert(moderate.sampleCount > 720, "auto mode increases angular samples"); assert(moderate.angularSampling.metricResolutionSatisfied, "moderate case becomes resolved"); assert(!moderate.angularSampling.autoCapped, "moderate case fits the cap"); const extreme = computeReceiveBeamPattern({ array: arrayFromPositions([[-10, 0, 0], [10, 0, 0]]), frequencyHz: 1_000_000, speedOfSound: 343, cut: "xz", steeringAngleDeg: 0, sampleCount: "auto", }); assertEqual(extreme.sampleCount, 14_400, "auto mode obeys the angular cap"); assert(extreme.angularSampling.autoCapped, "cap is explicit"); assert(!extreme.angularSampling.metricResolutionSatisfied, "extreme metric remains unresolved"); }); test("beam-pattern preflight rejects invalid input and excessive work", () => { const array = arrayFromPositions([[0, 0, 0], [0.1, 0, 0]]); let frequencyError = null; try { computeReceiveBeamPattern({ array, frequencyHz: 0 }); } catch (error) { frequencyError = error; } assert(frequencyError instanceof RangeError, "zero analog frequency is rejected"); let workError = null; try { computeReceiveBeamPattern({ array, frequencyHz: 1_000, sampleCount: 360, maximumEvaluations: 911, }); } catch (error) { workError = error; } assert(workError instanceof RangeError, "work cap is checked before evaluation"); assert(workError.message.includes("912"), "error reports maximum evaluation count"); let microphoneLimitError = null; try { computeReceiveBeamPattern({ array: arrayFromPositions( Array.from({ length: 1_025 }, (_, index) => [index / 1000, 0, 0]), ), frequencyHz: 1_000, }); } catch (error) { microphoneLimitError = error; } assert(microphoneLimitError instanceof RangeError, "microphone cap precedes pairwise audit"); assert( microphoneLimitError.message.includes("1024"), "microphone cap is explicit", ); let baselineError = null; try { computeReceiveBeamPattern({ array, frequencyHz: 1_000, maximumBaselineComparisons: 0, }); } catch (error) { baselineError = error; } assert(baselineError instanceof RangeError, "baseline pair cap is checked"); assert(baselineError.message.includes("1 projected-baseline"), "pair work is explicit"); let frequencyLimitError = null; try { computeReceiveBeamPattern({ array, frequencyHz: 1_000_001 }); } catch (error) { frequencyLimitError = error; } assert(frequencyLimitError instanceof RangeError, "extreme analog frequency is bounded"); assert(frequencyLimitError.message.includes("1000000"), "frequency cap is explicit"); }); test("co-located pattern accepts an exact sample-only evaluation cap", () => { const pattern = computeReceiveBeamPattern({ array: arrayFromPositions([[0, 0, 0]]), frequencyHz: 1_000, sampleCount: 360, maximumEvaluations: 360, }); assertEqual(pattern.evaluationCount, 360, "exact one-microphone sample work"); assertEqual(pattern.workload.maximumMetricEvaluationCount, 0, "constant cut reserves no HPBW work"); let error = null; try { computeReceiveBeamPattern({ array: arrayFromPositions([[0, 0, 0]]), frequencyHz: 1_000, sampleCount: 360, maximumEvaluations: 359, }); } catch (caught) { error = caught; } assert(error instanceof RangeError, "strict cap rejects one fewer evaluation"); }); function sphereFlatIndex(pattern, azimuthDeg, elevationDeg) { let azimuthIndex = -1; let elevationIndex = -1; for (let index = 0; index < pattern.azimuthAxisDeg.length; index += 1) { if (Math.abs(pattern.azimuthAxisDeg[index] - azimuthDeg) < 1e-10) { azimuthIndex = index; break; } } for (let index = 0; index < pattern.elevationAxisDeg.length; index += 1) { if (Math.abs(pattern.elevationAxisDeg[index] - elevationDeg) < 1e-10) { elevationIndex = index; break; } } assert(azimuthIndex >= 0, "requested azimuth is present on sphere grid"); assert(elevationIndex >= 0, "requested elevation is present on sphere grid"); return elevationIndex * pattern.azimuthSampleCount + azimuthIndex; } test("sphere directions use standard azimuth and elevation coordinates", () => { const cases = [ [0, 0, [1, 0, 0]], [90, 0, [0, 1, 0]], [180, 0, [-1, 0, 0]], [270, 0, [0, -1, 0]], [37, 90, [0, 0, 1]], [241, -90, [0, 0, -1]], ]; for (const [azimuthDeg, elevationDeg, expected] of cases) { const direction = receiveBeamPatternSphereDirection( azimuthDeg, elevationDeg, ); direction.forEach((value, axis) => assertNear( value, expected[axis], 1e-12, "sphere direction axis " + axis, )); } const diagonal = receiveBeamPatternSphereDirection(45, 30); assertNear(diagonal[0], Math.sqrt(6) / 4, 1e-12, "diagonal x"); assertNear(diagonal[1], Math.sqrt(6) / 4, 1e-12, "diagonal y"); assertNear(diagonal[2], 0.5, 1e-12, "diagonal z"); }); test("one microphone returns an unshaped raw full-sphere grid", () => { const pattern = computeReceiveBeamPatternSphere({ array: arrayFromPositions([[12.5, -7, 3]]), frequencyHz: 49_125, speedOfSound: 343, azimuthSampleCount: 12, elevationSampleCount: 7, maximumEvaluations: 84, }); assertEqual(pattern.steeringAzimuthDeg, 0, "default sphere steering azimuth"); assertEqual(pattern.steeringElevationDeg, 90, "default sphere steering is +Z"); assertEqual(pattern.steeringDirection.join(","), "0,0,1", "default direction"); assertEqual(pattern.gridShape.join(","), "7,12", "top-level grid shape"); assertEqual( pattern.flatIndexing, "elevation-major-row-major", "top-level flat indexing", ); assertEqual(pattern.grid.shape.join(","), "7,12", "elevation-major shape"); assertEqual(pattern.microphoneCount, 1, "sphere microphone provenance"); assertEqual(pattern.sampleCount, 84, "sphere sample product"); assertEqual(pattern.evaluationCount, 84, "exact sphere evaluation count"); assertEqual(pattern.responsePower.length, 84, "raw output sample count"); assertEqual(pattern.azimuthAxisDeg.at(-1), 330, "360 endpoint is not duplicated"); assertEqual(pattern.elevationAxisDeg[0], -90, "south pole is included"); assertEqual(pattern.elevationAxisDeg.at(-1), 90, "north pole is included"); assertEqual(pattern.grid.interpolation, "none", "no interpolation"); assertEqual(pattern.grid.smoothing, "none", "no smoothing"); assert( !pattern.grid.sampledMaximumNormalization, "sampled maximum normalization is disabled", ); assert(pattern.angularSampling.resolutionCriterionSatisfied, "zero aperture resolves"); for (let index = 0; index < pattern.sampleCount; index += 1) { assertNear(pattern.responseReal[index], 1, 0, "single-mic raw real"); assertNear(pattern.responseImaginary[index], 0, 0, "single-mic raw imaginary"); assertNear(pattern.responsePower[index], 1, 0, "single-mic raw power"); assertNear(pattern.responseLinear[index], 1, 0, "single-mic raw magnitude"); assertNear(pattern.responseDb[index], 0, 0, "single-mic raw dB"); assertNear(pattern.displayDb[index], 0, 0, "single-mic display dB"); } }); test("half-wavelength X baseline has the expected three-dimensional null surface", () => { const speedOfSound = 343; const frequencyHz = 1_000; const spacing = speedOfSound / frequencyHz / 2; const pattern = computeReceiveBeamPatternSphere({ array: arrayFromPositions([[-spacing / 2, 0, 0], [spacing / 2, 0, 0]]), frequencyHz, speedOfSound, azimuthSampleCount: 72, elevationSampleCount: 37, }); const positiveX = sphereFlatIndex(pattern, 0, 0); const positiveY = sphereFlatIndex(pattern, 90, 0); const negativeX = sphereFlatIndex(pattern, 180, 0); const northPole = sphereFlatIndex(pattern, 0, 90); const southPole = sphereFlatIndex(pattern, 0, -90); assertNear(pattern.responseLinear[positiveX], 0, 1e-12, "+X endfire null"); assertNear(pattern.responseLinear[negativeX], 0, 1e-12, "-X endfire null"); assertNear(pattern.responseLinear[positiveY], 1, 1e-12, "+Y broadside"); assertNear(pattern.responseLinear[northPole], 1, 1e-12, "+Z steering response"); assertNear(pattern.responseLinear[southPole], 1, 1e-12, "-Z front-back tie"); assert( pattern.angularSampling.resolutionCriterionSatisfied, "five-degree grid satisfies the disclosed cell-diagonal phase criterion", ); const expectedDiagonalChord = 2 * Math.hypot( Math.sin(2.5 * Math.PI / 180), Math.cos(2.5 * Math.PI / 180) * Math.sin(2.5 * Math.PI / 180), ); assertNear( pattern.angularSampling.maximumCellDiagonalPhaseStepRadBound, Math.PI * expectedDiagonalChord, 1e-12, "full-aperture cell-diagonal phase bound", ); for (let azimuthIndex = 1; azimuthIndex < pattern.azimuthSampleCount; azimuthIndex += 1) { assertNear( pattern.responsePower[northPole + azimuthIndex], pattern.responsePower[northPole], 0, "north-pole azimuth samples are physically coincident", ); assertNear( pattern.responsePower[southPole + azimuthIndex], pattern.responsePower[southPole], 0, "south-pole azimuth samples are physically coincident", ); } }); test("sphere raw complex response uses all three coordinates and the receive sign", () => { const speedOfSound = 343; const frequencyHz = 1_000; const wavelengthM = speedOfSound / frequencyHz; const positions = [ [0, 0, 0], [wavelengthM / 4, 0, wavelengthM / 16], [0, wavelengthM / 8, 3 * wavelengthM / 16], ]; const pattern = computeReceiveBeamPatternSphere({ array: arrayFromPositions(positions), frequencyHz, speedOfSound, steeringAzimuthDeg: 0, steeringElevationDeg: 90, azimuthSampleCount: 8, elevationSampleCount: 7, }); const targetDirection = receiveBeamPatternSphereDirection(45, 30); const steeringDirection = [0, 0, 1]; const centroid = [0, 0, 0]; for (const position of positions) { for (let axis = 0; axis < 3; axis += 1) { centroid[axis] += position[axis] / positions.length; } } let expectedReal = 0; let expectedImaginary = 0; for (const position of positions) { const centered = position.map((coordinate, axis) => coordinate - centroid[axis]); const phase = 2 * Math.PI / wavelengthM * ( centered[0] * (targetDirection[0] - steeringDirection[0]) + centered[1] * (targetDirection[1] - steeringDirection[1]) + centered[2] * (targetDirection[2] - steeringDirection[2]) ); expectedReal += Math.cos(phase) / positions.length; expectedImaginary += Math.sin(phase) / positions.length; } const targetIndex = sphereFlatIndex(pattern, 45, 30); assertNear(pattern.responseReal[targetIndex], expectedReal, 1e-12, "3D raw real"); assertNear( pattern.responseImaginary[targetIndex], expectedImaginary, 1e-12, "3D raw imaginary sign", ); assertNear( pattern.responsePower[targetIndex], expectedReal * expectedReal + expectedImaginary * expectedImaginary, 1e-12, "3D raw power", ); }); test("sphere samples agree with the existing XZ great-circle contract", () => { const array = arrayFromPositions([ [-0.12, 0.03, 0.04], [0.04, -0.08, 0.02], [0.15, 0.07, -0.11], ]); const shared = { array, frequencyHz: 3_200, speedOfSound: 343, }; const cut = computeReceiveBeamPattern({ ...shared, cut: "xz", steeringAngleDeg: 0, sampleCount: 360, }); const sphere = computeReceiveBeamPatternSphere({ ...shared, steeringAzimuthDeg: 0, steeringElevationDeg: 90, azimuthSampleCount: 4, elevationSampleCount: 3, }); const mappings = [ [0, 0, 90], [90, 0, 0], [180, 0, -90], [270, 180, 0], ]; for (const [cutAngleDeg, azimuthDeg, elevationDeg] of mappings) { const cutIndex = [...cut.anglesDeg].findIndex( (angleDeg) => angleDeg === cutAngleDeg, ); const sphereIndex = sphereFlatIndex(sphere, azimuthDeg, elevationDeg); assert(cutIndex >= 0, "mapped XZ angle is sampled"); assertNear( sphere.responseReal[sphereIndex], cut.responseReal[cutIndex], 1e-12, "sphere/XZ raw real at " + cutAngleDeg, ); assertNear( sphere.responseImaginary[sphereIndex], cut.responseImaginary[cutIndex], 1e-12, "sphere/XZ raw imaginary at " + cutAngleDeg, ); assertNear( sphere.responsePower[sphereIndex], cut.responsePower[cutIndex], 1e-12, "sphere/XZ raw power at " + cutAngleDeg, ); } }); test("sphere array factor remains invariant to translating a three-dimensional array", () => { const basePositions = [ [-0.12, 0.03, 0.04], [0.04, -0.08, 0.02], [0.15, 0.07, -0.11], ]; const translation = [10, -20, 5]; const translatedPositions = basePositions.map((position) => position.map((coordinate, axis) => coordinate + translation[axis])); const options = { frequencyHz: 7_500, speedOfSound: 343, steeringAzimuthDeg: 45, steeringElevationDeg: 30, azimuthSampleCount: 8, elevationSampleCount: 7, }; const base = computeReceiveBeamPatternSphere({ ...options, array: arrayFromPositions(basePositions), }); const translated = computeReceiveBeamPatternSphere({ ...options, array: arrayFromPositions(translatedPositions), }); for (let index = 0; index < base.sampleCount; index += 1) { assertNear( translated.responseReal[index], base.responseReal[index], 1e-11, "translation-invariant sphere real " + index, ); assertNear( translated.responseImaginary[index], base.responseImaginary[index], 1e-11, "translation-invariant sphere imaginary " + index, ); } }); test("sphere response preserves analog frequency and display floor only affects display dB", () => { const highFrequencyHz = 49_125; const speedOfSound = 343; const halfHighWavelength = speedOfSound / (2 * highFrequencyHz); const array = arrayFromPositions([ [-halfHighWavelength / 2, 0, 0], [halfHighWavelength / 2, 0, 0], ]); const shared = { array, speedOfSound, steeringElevationDeg: 90, azimuthSampleCount: 4, elevationSampleCount: 3, }; const highAnalog = computeReceiveBeamPatternSphere({ ...shared, frequencyHz: highFrequencyHz, dbFloor: -30, }); const highAnalogDeepFloor = computeReceiveBeamPatternSphere({ ...shared, frequencyHz: highFrequencyHz, dbFloor: -100, }); const foldedTemporalAlias = computeReceiveBeamPatternSphere({ ...shared, frequencyHz: 1_125, }); const positiveX = sphereFlatIndex(highAnalog, 0, 0); assertEqual(highAnalog.frequencyHz, highFrequencyHz, "analog frequency is preserved"); assertNear( highAnalog.wavelengthM, speedOfSound / highFrequencyHz, 1e-15, "sphere uses analog wavelength", ); assert( Math.abs( highAnalog.responseLinear[positiveX] - foldedTemporalAlias.responseLinear[positiveX] ) > 0.1, "sphere response is not computed from a folded ADC alias", ); for (let index = 0; index < highAnalog.sampleCount; index += 1) { assertNear( highAnalog.responsePower[index], highAnalogDeepFloor.responsePower[index], 0, "sphere raw power is display-floor independent", ); } assertEqual(highAnalog.displayDb[positiveX], -30, "sphere shallow display floor"); assertEqual( highAnalogDeepFloor.displayDb[positiveX], -100, "sphere deep display floor", ); assert( highAnalog.responseDb[positiveX] < -300, "sphere raw null is retained below display floors", ); }); test("coarse sphere grids expose failed resolution provenance without shaping raw samples", () => { const pattern = computeReceiveBeamPatternSphere({ array: arrayFromPositions([[-0.5, 0, 0], [0.5, 0, 0]]), frequencyHz: 100_000, speedOfSound: 343, azimuthSampleCount: 12, elevationSampleCount: 7, }); assert( !pattern.angularSampling.resolutionCriterionSatisfied, "coarse full-sphere phase-step criterion fails", ); assert( pattern.angularSampling.maximumCellDiagonalPhaseStepRadBound > pattern.angularSampling.phaseStepLimitRad, "failed cell-diagonal phase bound is exposed", ); assertEqual(pattern.responsePower.length, 84, "unresolved raw sphere is retained"); assertEqual( pattern.angularSampling.interpretation, "sampling-bound-only-not-proof-of-complete-lobe-detection", "resolution claim remains limited", ); }); test("sphere preflight checks axis, product, baseline, and evaluation caps before output allocation", () => { const single = arrayFromPositions([[0, 0, 0]]); const exact = computeReceiveBeamPatternSphere({ array: single, frequencyHz: 1_000, azimuthSampleCount: 4, elevationSampleCount: 3, maximumEvaluations: 12, }); assertEqual(exact.evaluationCount, 12, "exact sphere evaluation cap is accepted"); assertEqual( exact.workload.returnedFloat64Bytes, (12 * 6 + 4 + 3) * Float64Array.BYTES_PER_ELEMENT, "returned allocation provenance", ); let evaluationError = null; try { computeReceiveBeamPatternSphere({ array: single, frequencyHz: 1_000, azimuthSampleCount: 4, elevationSampleCount: 3, maximumEvaluations: 11, }); } catch (error) { evaluationError = error; } assert(evaluationError instanceof RangeError, "sphere evaluation cap rejects work"); assert(evaluationError.message.includes("12"), "evaluation requirement is explicit"); let productError = null; try { computeReceiveBeamPatternSphere({ array: single, frequencyHz: 1_000, azimuthSampleCount: 720, elevationSampleCount: 721, }); } catch (error) { productError = error; } assert(productError instanceof RangeError, "sphere sample product is bounded"); assert( productError.message.includes("262144"), "sphere sample-product limit is explicit", ); let axisError = null; try { computeReceiveBeamPatternSphere({ array: single, frequencyHz: 1_000, azimuthSampleCount: 1_441, elevationSampleCount: 3, }); } catch (error) { axisError = error; } assert(axisError instanceof RangeError, "sphere azimuth axis is bounded"); let baselineError = null; try { computeReceiveBeamPatternSphere({ array: arrayFromPositions([[0, 0, 0], [0.1, 0, 0]]), frequencyHz: 1_000, azimuthSampleCount: 4, elevationSampleCount: 3, maximumBaselineComparisons: 0, }); } catch (error) { baselineError = error; } assert(baselineError instanceof RangeError, "sphere baseline work is bounded"); assert( baselineError.message.includes("1 spatial-baseline"), "sphere baseline requirement is explicit", ); let steeringError = null; try { computeReceiveBeamPatternSphere({ array: single, frequencyHz: 1_000, steeringElevationDeg: 91, azimuthSampleCount: 4, elevationSampleCount: 3, }); } catch (error) { steeringError = error; } assert(steeringError instanceof RangeError, "invalid sphere elevation is rejected"); });