Releases: jpurnell/BusinessMath
v2.1.4: Hygiene Cleanup Follow-ups
What's New
BusinessMath v2.1.4 completes the three cleanup items deferred from v2.1.3:
Examples/ format-string cleanup
MultipleLinearRegressionExample.swift: 22 calls →value.number(N)LinearRegressionConvenienceExample.swift: 18 calls →value.number(N)
Now consistent with the rest of the codebase. Examples aren't part of CI but are user-facing reference code that propagated the bad pattern.
generateRandomReturns seeded overload
- New:
generateRandomReturns(count:mean:stdDev:using: inout some RandomNumberGenerator) - Existing unseeded overload now wraps the new one with a
SystemRandomNumberGenerator— same observable behavior, no breaking change - Fixed an edge case where
Double.random(in: 0.0...1.0)returning exactly 0 would causelog(0) = -infin Box-Muller (guarded withDouble.leastNormalMagnitude)
SeededRNG consolidation
5 test files had inlined a local struct SeededRNG with the MMIX LCG. Consolidated into:
- New:
TestSupport.MMIXSeededRNG— value type withnext()returning[0, 1]andnextSigned()returning[-1, 1] - 5 affected files updated to import
TestSupportand useMMIXSeededRNG - Bit-identical sequences preserved — no test assertions needed re-tuning
- Existing
TestSupport.SeededRNG(Numerical Recipes LCG) untouched
Compatibility
- Purely additive at the public API level. No types renamed, no signatures changed.
- All 4817 tests from v2.1.3 continue to pass. Total test count unchanged.
Future Cleanup Tracked
DocC .md files in Sources/BusinessMath/BusinessMath.docc/ still contain ~62 String(format:) instances in Swift code samples (12 files). These are documentation, not compiled, but propagate the pattern to users copying from the docs. Tracked as a future v2.1.5 cleanup.
Platforms
CI verified on macOS 15 and Ubuntu 24.04. All 4 CI checks green.
v2.1.3: Dev-Hygiene Cleanup
What's New
BusinessMath v2.1.3 is a developer-hygiene patch release. No public API changes, no production behavior changes. Cleans up technical debt that v2.1.1 and v2.1.2 work surfaced but didn't address.
Fixed
All String(format:) violations removed from Sources/ and Tests/
13 source files and 4 test files were silently using the banned C-style format ABI, which is on the Forbidden Patterns list in 01_CODING_RULES.md because of recurring SIGSEGV crashes when %s is given a Swift String. All call sites now use:
- The project's existing
value.number(N)extension (FloatingPointFormatStyleon iOS 15+/macOS 12+,NumberFormatterfallback) - A new private POSIX-locale
NumberFormatterhelper insideFloatingPointFormatter.swift(drop-inprintfsemantics for the smart-rounding paths) String(value, radix: 16)with manual zero-padding for the one hex-formatting case inTemplateRegistry
Flaky PortfolioUtilitiesTests test fixed
PortfolioUtilitiesTests.Random returns are within reasonable range previously used Double.random(in:) (the system RNG), which violates the mandatory deterministic-randomness rule from 09_TEST_DRIVEN_DEVELOPMENT.md and occasionally drew values 5σ+ outside the test's expected range. Refactored to use the existing TestSupport.SeededRNG (LCG, seed 42), making the test fully deterministic and auditable.
Two pre-existing tests tightened
-
Accelerate FFT: matches Pure Swiftpreviously only checked peak bin location, which let the v2.1.0 4× scaling bug slip through (the peak was at the right bin, just 4× too large). Tightened to require absolute bin-for-bin equivalence at1e-9relative tolerance. The v2.1.1 PSD work fixed the underlying bug; this test now locks the fix in. -
Parseval's theoremtest previously used0.5 < ratio < 2.0— a 2× margin in either direction so loose it would have passed even with major numerical bugs. Tightened to1e-12relative tolerance, which is what Parseval's theorem actually guarantees for the discrete formulation.
Why this release exists
The format-string detection check filed for quality-gate-swift would have failed against the BusinessMath production codebase with 17 hits the moment it shipped. v2.1.3 unblocks that check from going live.
Compatibility
- Purely additive at the public API level. No types, methods, or signatures changed.
- All 4720 tests from v2.1.1 plus the 97 new interpolation tests from v2.1.2 continue to pass. Total test count remains 4817.
Examples/directory still hasString(format:)violations and will be cleaned up in a separate PR — Examples are not part of the package build target and don't run in CI.
Platforms
CI verified on macOS 15 and Ubuntu 24.04. All four CI checks green: tests, release build, Apple platform compile, Linux release compile.
v2.1.2: 1D Interpolation Module
What's New
BusinessMath v2.1.2 introduces the comprehensive 1D interpolation module designed from day one to extend cleanly to N-dimensional gridded and scattered interpolation in future releases without breaking changes.
Driven by HRV frequency-domain analysis (BioFeedbackKit), but the interpolation primitive is broadly useful far beyond HRV.
Vector1D — completing the vector type family
Vector1D<T> is a trivial 1-dimensional VectorSpace conformer that completes the fixed-dimension family alongside Vector2D, Vector3D, and VectorN. Lets generic algorithms over VectorSpace include the 1D scalar case naturally instead of special-casing scalars. Initializer Vector1D(2.5), stored property .value.
Interpolator protocol
Single root protocol with Point: VectorSpace and Value: Sendable associated types. v2.1.2 conformers all use Point = Vector1D<T>. Future 2D conformers will use Point = Vector2D<T>, ND scattered will use Point = VectorN<T>. No protocol changes ever — additive only.
Ten 1D interpolation methods, scalar-output
| Method | Notes |
|---|---|
NearestNeighborInterpolator |
Closest known value |
PreviousValueInterpolator |
Step function holding previous |
NextValueInterpolator |
Step function holding next (pass-through at exact knots) |
LinearInterpolator |
Piecewise linear |
CubicSplineInterpolator |
Four boundary conditions: .natural (default — Kubios HRV), .notAKnot (MATLAB default), .clamped(left:right:), .periodic |
PCHIPInterpolator |
Fritsch–Carlson monotone cubic. Overshoot-safe. The scipy-recommended safe cubic. |
AkimaInterpolator |
With modified: Bool = true (makima default) |
CatmullRomInterpolator |
Cardinal spline with default tension = 0 (standard Catmull-Rom) |
BSplineInterpolator |
Configurable degree 1..5 (cubic default) |
BarycentricLagrangeInterpolator |
Numerically stable polynomial, suitable for small N ≤ 20 |
Ten 1D interpolation methods, vector-output
Vector*Interpolator flavors of all 10. Each takes ys: [VectorN<T>] and produces VectorN<T> output. Use cases:
- 3-axis accelerometer over time → 3-component vector per sample
- Multi-channel EEG over time → N-component vector per sample
- Stock portfolio historical values → M-component vector per sample
- Multi-sensor fusion → K-component vector per sample
The vector flavors run the scalar algorithm once per output channel via internal channel transposition, so they're correct by construction.
Supporting types
ExtrapolationPolicy<T>:.clamp(default),.extrapolate,.constant(T)InterpolationError: thrown only at construction time, never during evaluation
Test coverage
- 97 new tests across 11 suites, all passing
- Full suite: 4817 / 4817 passing, zero compiler warnings on macOS and Linux
- Pass-through invariant on every method
- Linear-data invariant (reproduces
a*x + bexactly to machine precision) on every cubic and polynomial method - Hand-computed reference values from
Tests/Validation/Interpolation_Playground.swiftlocked at1e-12 - Monotonicity preservation verified for PCHIP and Akima
- Per-channel equivalence verified for all 10 vector-output flavors
- Cross-method consistency: BSpline degree=1 matches LinearInterpolator, BSpline degree=3 matches CubicSpline.notAKnot
- All four CubicSpline boundary conditions tested
- Error path coverage
Architecture decisions
Documented in Instruction Set/00_CORE_RULES/10_ARCHITECTURE_DECISIONS.md:
- ADR-001: Add Vector1D to complete the vector type family
- ADR-002: Single Interpolator protocol with Point/Value associated types
- ADR-003: Multi-version roadmap for ND interpolation (1D in v2.1.2, 2D in v2.2, 3D and ND gridded in v2.3, ND scattered in v2.4)
- ADR-004: Method set and parameter defaults
Bugs caught by the validation playground
The standalone validation playground caught two bugs before any package code was written:
- NextValue at exact knots was returning
ys[i+1]instead ofys[i]. Fixed. - CatmullRom default tension was 0.5 in the original ADR, which is half-strength tangents and fails the linear-data invariant by 12.6%. Standard Catmull-Rom is
τ = 0(full-strength tangents). ADR-004 amended; default corrected.
A third bug surfaced during test execution and was fixed in the same PR:
- PreviousValue at the last exact knot was returning
ys[n-2]. Fixed with a special-case check fort == xs[hi].
Compatibility
Purely additive at the public API level. No existing types or methods were changed. All 4720 tests from v2.1.1 continue to pass, and v2.1.2 brings the total to 4817.
Platforms
CI verified on macOS 15 and Ubuntu 24.04. All checks green: tests, release build, Apple platform compile, Linux release compile.
v2.1.1: Power Spectral Density + AccelerateFFTBackend scaling fix
What's New
BusinessMath v2.1.1 adds normalized power spectral density (PSD) to the FFT layer and fixes a pre-existing 4× scaling bug in AccelerateFFTBackend.powerSpectrum that was uncovered while implementing PSD.
Driven by the BioFeedbackKit project, which needs physically meaningful spectral magnitudes (ms² for HRV LF/HF analysis) without every consumer reinventing FFT normalization.
Added
FFTBackend.powerSpectralDensity(_:sampleRate:)
New protocol method returning a one-sided PSD in units²/Hz. The integral over frequency equals the time-domain variance of a zero-mean input (Parseval's theorem), so band powers come out in physical units directly.
let backend = FFTBackendSelector.selectBackend()
let mean = signal.reduce(0, +) / Double(signal.count)
let zeroMean = signal.map { $0 - mean }
let psd = backend.powerSpectralDensity(zeroMean, sampleRate: 4.0)Default implementation in an extension — all conformers (PureSwiftFFTBackend, AccelerateFFTBackend) inherit it for free.
Correct M-vs-N normalization
The PSD method uses the unpadded signal length M for normalization, not the internally zero-padded length N. This ensures PSD integrals are physically meaningful regardless of whether the input length is a power of 2. Previously, every downstream consumer needed to know about this gotcha; now BusinessMath handles it.
PSDBin value type and powerSpectralDensityBins(_:sampleRate:) convenience
Pairs each PSD value with its center frequency in Hz, so downstream band-integration code doesn't need to compute bin spacing manually.
let bins = backend.powerSpectralDensityBins(zeroMean, sampleRate: 4.0)
let lfPower = bins
.filter { $0.frequency >= 0.04 && $0.frequency < 0.15 }
.reduce(0.0) { $0 + $1.power }
* (4.0 / Double((bins.count - 1) * 2)) // × ΔfFixed
AccelerateFFTBackend 4× power scaling bug
vDSP_fft_zripD (the real-input FFT primitive) returns outputs scaled by 2 vs the textbook DFT formula — this is a documented vDSP convention for packed real-input transforms. Squaring magnitudes therefore produced |X[k]|² values 4× the textbook result on Darwin only.
The existing cross-backend test only checked peak bin location (purePeak == accelPeak), not absolute magnitudes, so the discrepancy was invisible until the new PowerSpectralDensityTests cross-backend equivalence test surfaced it.
Fix: apply a × 0.25 correction to all DC, Nyquist, and typical bins in AccelerateFFTBackend.powerSpectrum. Both backends now satisfy Parseval's theorem to machine precision and produce identical PSD values to within 1e-9 relative tolerance.
Impact on consumers: any code that compared absolute spectrum magnitudes from AccelerateFFTBackend against external references (SciPy, MATLAB, published HRV papers, etc.) was previously wrong by 4×. After this fix, it's correct.
Test Coverage
- 12 new tests in
Tests/BusinessMathTests/StreamingTests/PowerSpectralDensityTests.swift:- Parseval's theorem on pure sine waves, two-tone signals, and mean-removed inputs
- The critical M=50-padded-to-64 zero-padding edge case
- DC and Nyquist bin handling (no factor-of-2 in one-sided convention)
- Cross-backend equivalence (PureSwift vs Accelerate)
- PSDBin frequency labeling and convenience method
- Standalone validation playground at
Tests/Validation/PSD_Validation.swifthand-rolls the full PSD pipeline with no BusinessMath dependency, so the formulas are verified independently against Parseval before any package code runs. - Full suite: 4720/4720 passing, zero compiler warnings.
Compatibility
Purely additive at the public API level. The existing powerSpectrum(_:) signature is unchanged. The Accelerate scaling fix is a bug correction, not a behavior change — any consumer that relied on the old 4× values for internal consistency will continue to work (the ratios between bins are unchanged; only the absolute scale is corrected).
Platforms
Tested on macOS 15 and Ubuntu 24.04 via CI. All 4720 tests pass on both platforms.
v2.1 Real-Time Streaming Enhancements
BusinessMath v2.1.0 extends the streaming layer with time-aware operations, making it possible to process real-time signals from sensors, financial feeds, and IoT devices directly in Swift.
What's New
Timestamped Streams
Every streaming operator can now carry timestamp metadata. The new Timestamped<T> type pairs any value with a precise ContinuousClock.Instant, enabling operations that depend on when data arrived — not just what it contains.
Time-Based Windowing
Group stream elements by elapsed time rather than element count. Tumbling windows collect non-overlapping buckets; sliding windows provide overlapping views with configurable stride. Both handle irregular arrival rates correctly.
Successive Difference Analysis
New operators for analyzing the changes between consecutive values — including rolling root-mean-square of differences (RMSSD) and threshold exceedance rates. These use O(1) incremental accumulators that maintain constant performance regardless of window size.
Multi-Rate Stream Alignment
Correlate two streams running at different sample rates. A slow signal (~1 Hz) can be aligned with a fast signal (50+ Hz) using nearest-neighbor snapping or linear interpolation — no manual timestamp matching required.
Frequency Domain Analysis
Apply FFT to windowed time series to extract power spectra. Query specific frequency bands to measure energy concentration. The FFTBackend protocol automatically selects Apple's Accelerate framework on Darwin platforms and falls back to a pure Swift implementation on Linux and Android.
Faster Rolling Statistics
Existing rolling mean, variance, and sum operators have been upgraded from O(window) to O(1) per element using incremental accumulators with reverse-eviction, improving throughput for high-frequency data.
CI Improvements
- All CI jobs now run in parallel (down from sequential), reducing push feedback time from ~45 minutes to ~15 minutes
- Release-mode tests moved to a twice-daily scheduled run, keeping push CI focused and fast
Testing
108 new streaming tests covering golden-path behavior, edge cases (NaN, Infinity, empty streams), numerical stability (1e-15 to 1e15 scale), mathematical invariants, and stress tests up to 100,000 elements.
Full Changelog: v2.0.0...v2.1.0
v2.0.0
BusinessMath v2.0.0 - Production-Ready Financial Modeling
Major release bringing GPU-accelerated optimization, role-based financial statements, and comprehensive security hardening to Swift.
Highlights
- Role-Based Financial Statements - Accounts can have multiple roles across IS, BS, and CFS
- GPU Acceleration - 10-100x speedup for genetic algorithms on Apple Silicon
- 100% Documentation - All 198+ public APIs fully documented
- Security Hardening - Division-by-zero safety, force unwrap elimination
- 4,558+ Tests - Full Swift 6 concurrency compliance
- Linux Support - Cross-platform with swift-crypto fallback
New Features
- Multiple linear regression with GPU-accelerated matrix backends
- Contribution margin analysis & break-even calculations
- Pro forma adjustments for LBO/M&A modeling
- Working capital tracking helpers
- Debt classification by instrument type
- Async streaming for Monte Carlo simulations
- 4 new optimizers (Adaptive, MultiStart, Async, Benchmark)
Breaking Changes
- Financial statements use role-based API (
incomeStatementRole:instead oftype:) - Optimization parameters unified to
initialGuess - Error types consolidated into
FinancialModelError
Migration time: 1-3 hours for most projects. See MIGRATION_GUIDE_v2.0.md.
Installation
.package(url: "https://github.com/jpurnell/BusinessMath.git", from: "2.0.0")
Requirements: Swift 6.0+ | iOS 14+ | macOS 13+ | Linuxv2.0.0-rc.9
Full Changelog: v2.0.0-rc.8...v2.0.0-rc.9
v2.0.0-rc.8
Cleaning up for full linux compatibility
Full Changelog: v2.0.0-rc.7...v2.0.0-rc.8
v2.0.0-rc.7
Full Changelog: v2.0.0-rc.6...v2.0.0-rc.7
v2.0.0-rc.6
Full Swift 6 Concurrency for both library and tests, with no warnings
Full Changelog: v2.0.0-rc.5...v2.0.0-rc.6