v2.1.1: Add powerSpectralDensity, fix Accelerate 4× scaling bug#5
Merged
v2.1.1: Add powerSpectralDensity, fix Accelerate 4× scaling bug#5
Conversation
vDSP_fft_zripD returns FFT outputs scaled by 2 vs the textbook DFT formula (vDSP convention for packed real-input FFT). Squaring magnitudes therefore produced values 4× the textbook |X[k]|² on Darwin only, breaking absolute-power analysis (Parseval's theorem, PSD integration, band power, etc.). The pre-existing cross-backend test only checked peak bin location, not absolute magnitudes, so the discrepancy was invisible. The new PowerSpectralDensityTests suite (added in the next commit) is the lever that surfaced it. Fix: apply ×0.25 correction to all DC, Nyquist, and typical bins in AccelerateFFTBackend.powerSpectrum. Both backends now produce identical absolute power values to within machine precision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a normalized one-sided power spectral density method to the FFTBackend protocol so downstream consumers (BioFeedbackKit, etc.) get physically meaningful spectral values directly without reinventing normalization. API additions: - FFTBackend.powerSpectralDensity(_:sampleRate:) — one-sided PSD in units²/Hz. Default implementation in an extension; all backends inherit for free. - FFTBackend.powerSpectralDensityBins(_:sampleRate:) — convenience that pairs each PSD value with its center frequency in Hz. - PSDBin value type — Sendable, Equatable, (frequency: Double, power: Double). Normalization correctness: - One-sided spectrum: DC and Nyquist bins use edge factor 1/(M·fs); typical bins use 2/(M·fs). - Uses the UNPADDED signal length M, not the internally zero-padded length N. This ensures the PSD integral equals the time-domain variance per Parseval's theorem regardless of input length. Previously every downstream consumer had to know about this gotcha. Testing: - 12 new tests in PowerSpectralDensityTests.swift covering Parseval on pure sines, two-tone signals, the M=50→64 zero-padding edge case, DC and Nyquist bin handling, cross-backend equivalence (PureSwift vs Accelerate, both now satisfy Parseval after the previous commit's scaling fix), empty/invalid input handling, and PSDBin convenience. - Validation playground at Tests/Validation/PSD_Validation.swift — standalone hand-rolled implementation that verifies the formulas against Parseval's theorem independent of BusinessMath, producing the exact values the test suite asserts. - Full BusinessMath suite: 4720/4720 passing, zero compiler warnings. Purely additive — no breaking changes. Existing powerSpectrum(_:) signature is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds normalized power spectral density (PSD) to
FFTBackendand fixes a pre-existing 4× scaling bug inAccelerateFFTBackend.powerSpectrumthat was uncovered while implementing PSD.Driven by the BioFeedbackKit project, which needs physically meaningful spectral magnitudes (ms² for HRV LF/HF analysis) without reinventing FFT normalization in every downstream consumer.
Changes (2 commits)
vDSP_fft_zripDreturns FFT outputs scaled by 2 vs the textbook DFT formula. Squaring magnitudes produced|X[k]|²that was 4× too large on Darwin. Fix: ×0.25 correction in the power computation. The existing cross-backend test only checked peak bin location, so the discrepancy was invisible.powerSpectralDensity(_:sampleRate:)toFFTBackend— new protocol method with default implementation in an extension. Returns one-sided PSD inunits²/Hz; integral over frequency equals the time-domain variance per Parseval's theorem. Uses the unpadded signal lengthMfor normalization, so PSD values are correct regardless of zero-padding. Also addspowerSpectralDensityBins(_:sampleRate:)convenience and thePSDBinvalue type.Test plan
PowerSpectralDensityTests.swiftcover Parseval on multiple fixtures, the M-vs-N zero-padding edge case (M=50 padded to 64), DC and Nyquist edge factors, and cross-backend equivalenceTests/Validation/PSD_Validation.swiftverifies formulas independently against Parseval's theorem before any package code runs1e-9relative tolerance; both satisfy Parseval to machine precisionNon-breaking
Purely additive at the public API level. The existing
powerSpectrum(_:)signature is unchanged. Consumers ofAccelerateFFTBackendthat were comparing absolute spectrum magnitudes against external references were previously wrong by 4×; they should see corrected (smaller by 4×) values after this fix.Findings surfaced for separate handling
PortfolioUtilitiesTests.Random returns are within reasonable rangeuses unseeded randomness, violating the mandatory TDD rule in09_TEST_DRIVEN_DEVELOPMENT.md. Observed one failure during development runs. Worth its own PR with a seeded RNG.accelerateMatchesPureSwifttest only checks peak bin location. After this fix, it could be tightened to assert absolute equivalence. Worth its own PR.🤖 Generated with Claude Code