From f0d668f0d6bc7c6aa110dadf0472b529b2751289 Mon Sep 17 00:00:00 2001 From: Justin Purnell Date: Tue, 7 Apr 2026 17:52:57 -1000 Subject: [PATCH] v2.1.3: dev-hygiene cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the remaining 13 String(format:) violations in Sources/ and 4 in Tests/ — banned by 01_CODING_RULES.md because of recurring SIGSEGV crashes when %s is given a Swift String. All call sites now use the project's value.number(N) extension or, in FloatingPointFormatter.swift, a private POSIX-locale NumberFormatter helper. Fixes the previously-flaky PortfolioUtilitiesTests "Random returns are within reasonable range" test that violated the mandatory deterministic- randomness rule. Now uses TestSupport.SeededRNG with seed 42. Tightens two pre-existing loose tests: - accelerateMatchesPureSwift: was peak-bin-only, missed the v2.1.0 4× scaling bug. Now bin-for-bin equivalence at 1e-9 relative. - parsevalsTheorem: was 0.5..2.0 ratio (2× margin in either direction). Now 1e-12 relative — what the theorem actually guarantees. No public API changes, no production behavior changes. All 4817 tests pass, zero compiler warnings. Examples/ still has String(format:) violations and will be cleaned up separately — not part of package build/CI. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 55 +++++++++++++++++++ .../Diagnostics/ModelDebugger.swift | 9 ++- .../Financial Statements/DebtCovenants.swift | 8 +-- .../Fluent API/ModelBuilder.swift | 14 ++--- .../Templates/TemplateRegistry.swift | 6 +- .../Heuristic/GPU/MetalBuffers.swift | 2 +- .../IntegerProgramming/BranchAndBound.swift | 2 +- .../LinearProgramming/SimplexSolver.swift | 2 +- .../Optimization/LinearityValidation.swift | 2 +- .../Optimization/PerformanceBenchmark.swift | 20 +++---- .../Performance/CalculationCache.swift | 2 +- .../Regression/ModelValidation.swift | 4 +- .../Formatting/FloatingPointFormatter.swift | 25 +++++++-- .../CommandLineVisualization.swift | 8 --- .../MultivariateLBFGSTests.swift | 2 +- .../PortfolioUtilitiesTests.swift | 28 ++++++++-- .../GPUPerformanceBenchmark.swift | 8 ++- .../MonteCarloGPUPerformanceTests.swift | 16 +++--- .../MatrixBackendBenchmarks.swift | 12 ++-- .../StreamingFrequencyDomainTests.swift | 35 +++++++----- 20 files changed, 178 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3fd4cd..058f0f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## BusinessMath Library +### [2.1.3] - 2026-04-07 + +**Version 2.1.3** is a developer-hygiene release that cleans up the +remaining `String(format:)` violations in production source and tests +(forbidden by `01_CODING_RULES.md`), fixes a previously-flaky test that +violated the mandatory deterministic-randomness rule from +`09_TEST_DRIVEN_DEVELOPMENT.md`, and tightens two pre-existing tests +that were too loose to catch real numerical bugs. + +#### Fixed + +- **All `String(format:)` violations in `Sources/` and `Tests/` removed.** + 13 source files and 4 test files were silently using the banned + C-style format ABI, which is on the Forbidden Patterns list because + of recurring SIGSEGV crashes when `%s` is given a Swift `String`. + All call sites now use the project's `value.number(N)` extension or, + in `FloatingPointFormatter.swift`, a private POSIX-locale + `NumberFormatter` helper that's a true drop-in replacement for + `printf` semantics. + +- **Flaky `PortfolioUtilitiesTests.Random returns are within reasonable range` + test fixed.** Previously used `Double.random(in:)` (the system RNG), + which violates the mandatory deterministic-randomness rule and + occasionally drew values 5σ+ outside the test's expected range, + failing CI ~once every few runs. Refactored to use the existing + `TestSupport.SeededRNG` (LCG with seed 42), making the test fully + deterministic and auditable. + +- **`Accelerate FFT: matches Pure Swift within tolerance`** previously + only checked peak bin location, which let the v2.1.0 4× scaling bug + in `AccelerateFFTBackend` slip through (the peak was at the right bin, + just 4× too large). Tightened to require absolute bin-for-bin + agreement at `1e-9` relative tolerance. The v2.1.1 PSD work fixed the + underlying scaling bug; this test now locks the fix in. + +- **`Parseval's theorem` test** previously used `0.5 < ratio < 2.0` as + its tolerance — a 2× margin in either direction so loose it would + have passed even with major numerical bugs. Tightened to `1e-12` + relative tolerance, which is what Parseval's theorem actually + guarantees for the discrete formulation. + +#### Notes + +- Purely a hygiene release. No public API changes, no behavior changes + in production code paths. +- All 4720 tests from v2.1.1 and the 97 new interpolation tests from + v2.1.2 continue to pass. Total test count remains 4817. +- `Examples/` directory still has `String(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. +- This release unblocks the `c-style-format-string` SafetyAuditor check + filed for quality-gate-swift — that check can now ship without + generating false positives against the BusinessMath production + codebase. + ### [2.1.2] - 2026-04-07 **Version 2.1.2** introduces the comprehensive 1D interpolation module diff --git a/Sources/BusinessMath/Diagnostics/ModelDebugger.swift b/Sources/BusinessMath/Diagnostics/ModelDebugger.swift index c4a01733..5dd939d6 100644 --- a/Sources/BusinessMath/Diagnostics/ModelDebugger.swift +++ b/Sources/BusinessMath/Diagnostics/ModelDebugger.swift @@ -285,8 +285,7 @@ public actor ModelDebugger { if !value.isNaN && !value.isInfinite && percentDifference > tolerance { issues.append(DiagnosticIssue( severity: .error, - message: String(format: "Value differs from expected by %.2f%% (tolerance: %.2f%%)", - percentDifference * 100, tolerance * 100), + message: "Value differs from expected by \((percentDifference * 100).number(2))% (tolerance: \((tolerance * 100).number(2))%)", location: context, suggestion: "Review calculation logic and input values" )) @@ -1087,9 +1086,9 @@ public struct Explanation: Sendable { if let ctx = context { output += "Context: \(ctx)\n" } - output += String(format: "Actual: %.2f\n", actual) - output += String(format: "Expected: %.2f\n", expected) - output += String(format: "Difference: %.2f (%.2f%%)\n\n", difference, percentageDifference) + output += "Actual: \(actual.number(2))\n" + output += "Expected: \(expected.number(2))\n" + output += "Difference: \(difference.number(2)) (\(percentageDifference.number(2))%)\n\n" if !possibleReasons.isEmpty { output += "Possible Reasons:\n" diff --git a/Sources/BusinessMath/Financial Statements/DebtCovenants.swift b/Sources/BusinessMath/Financial Statements/DebtCovenants.swift index ee392832..7f80da20 100644 --- a/Sources/BusinessMath/Financial Statements/DebtCovenants.swift +++ b/Sources/BusinessMath/Financial Statements/DebtCovenants.swift @@ -602,8 +602,8 @@ extension Array where Element == CovenantComplianceResult { report += "VIOLATIONS:\n" for (index, violation) in violations.enumerated() { report += " \(index + 1). \(violation.covenant.name)\n" - report += " Actual: \(String(format: "%.2f", violation.actualValue))\n" - report += " Required: \(String(format: "%.2f", violation.requiredValue))\n" + report += " Actual: \(violation.actualValue.number(2))\n" + report += " Required: \(violation.requiredValue.number(2))\n" } report += "\n" } @@ -612,8 +612,8 @@ extension Array where Element == CovenantComplianceResult { report += "COMPLIANT:\n" for (index, result) in compliant.enumerated() { report += " \(index + 1). \(result.covenant.name)\n" - report += " Actual: \(String(format: "%.2f", result.actualValue))\n" - report += " Required: \(String(format: "%.2f", result.requiredValue))\n" + report += " Actual: \(result.actualValue.number(2))\n" + report += " Required: \(result.requiredValue.number(2))\n" } } diff --git a/Sources/BusinessMath/Fluent API/ModelBuilder.swift b/Sources/BusinessMath/Fluent API/ModelBuilder.swift index db875064..a5c374a5 100644 --- a/Sources/BusinessMath/Fluent API/ModelBuilder.swift +++ b/Sources/BusinessMath/Fluent API/ModelBuilder.swift @@ -127,7 +127,7 @@ public struct FinancialModel: Sendable { DebugContext.shared.recordStep( operation: "GetAccount(\(component.name))", input: "Period(\(period.label))", - output: String(format: "%.0f", value) + output: value.number(0) ) return sum + value @@ -136,8 +136,8 @@ public struct FinancialModel: Sendable { // Record sum operation DebugContext.shared.recordStep( operation: "Sum(Revenue Accounts)", - input: "[\(accountValues.map { String(format: "%.0f", $0) }.joined(separator: ", "))]", - output: String(format: "%.0f", total) + input: "[\(accountValues.map { $0.number(0) }.joined(separator: ", "))]", + output: total.number(0) ) return total @@ -160,8 +160,8 @@ public struct FinancialModel: Sendable { // Record individual expense access DebugContext.shared.recordStep( operation: "GetExpense(\(component.name))", - input: "Period(\(period.label)), Revenue(\(String(format: "%.0f", revenue)))", - output: String(format: "%.0f", value) + input: "Period(\(period.label)), Revenue(\(revenue.number(0)))", + output: value.number(0) ) return sum + value @@ -170,8 +170,8 @@ public struct FinancialModel: Sendable { // Record sum operation DebugContext.shared.recordStep( operation: "Sum(Expense Accounts)", - input: "[\(expenseValues.map { String(format: "%.0f", $0) }.joined(separator: ", "))]", - output: String(format: "%.0f", total) + input: "[\(expenseValues.map { $0.number(0) }.joined(separator: ", "))]", + output: total.number(0) ) return total diff --git a/Sources/BusinessMath/Fluent API/Templates/TemplateRegistry.swift b/Sources/BusinessMath/Fluent API/Templates/TemplateRegistry.swift index a6bb75c9..a335fc8b 100644 --- a/Sources/BusinessMath/Fluent API/Templates/TemplateRegistry.swift +++ b/Sources/BusinessMath/Fluent API/Templates/TemplateRegistry.swift @@ -317,7 +317,11 @@ public struct TemplatePackage: Codable, Sendable { /// Calculate SHA-256 checksum for template data static func calculateChecksum(_ data: String) -> String { let hash = SHA256.hash(data: Data(data.utf8)) - return hash.compactMap { String(format: "%02x", $0) }.joined() + // Format each byte as 2-digit lowercase hex via String(_:radix:uppercase:) + return hash.map { byte in + let hex = String(byte, radix: 16, uppercase: false) + return hex.count == 1 ? "0\(hex)" : hex + }.joined() } } diff --git a/Sources/BusinessMath/Optimization/Heuristic/GPU/MetalBuffers.swift b/Sources/BusinessMath/Optimization/Heuristic/GPU/MetalBuffers.swift index 086d5256..c3a259b2 100644 --- a/Sources/BusinessMath/Optimization/Heuristic/GPU/MetalBuffers.swift +++ b/Sources/BusinessMath/Optimization/Heuristic/GPU/MetalBuffers.swift @@ -181,7 +181,7 @@ internal final class MetalBuffers { var memoryDescription: String { let bytes = totalMemoryAllocated let mb = Double(bytes) / (1024 * 1024) - return String(format: "%.2f MB", mb) + return "\(mb.number(2)) MB" } } #endif diff --git a/Sources/BusinessMath/Optimization/IntegerProgramming/BranchAndBound.swift b/Sources/BusinessMath/Optimization/IntegerProgramming/BranchAndBound.swift index 9bdf4bcd..3d95df01 100644 --- a/Sources/BusinessMath/Optimization/IntegerProgramming/BranchAndBound.swift +++ b/Sources/BusinessMath/Optimization/IntegerProgramming/BranchAndBound.swift @@ -1044,7 +1044,7 @@ public struct BranchAndBoundSolver where V.Scalar == Double, V: } // Deduplicate: check if we've seen this cut before - let cutSignature = "\(cut.coefficients.map { String(format: "%.6f", $0) }.joined(separator:",")):\(String(format: "%.6f", cut.rhs))" + let cutSignature = "\(cut.coefficients.map { $0.number(6) }.joined(separator:",")):\(cut.rhs.number(6))" if !generatedCuts.contains(cutSignature) { cutsThisRound.append(cut) diff --git a/Sources/BusinessMath/Optimization/LinearProgramming/SimplexSolver.swift b/Sources/BusinessMath/Optimization/LinearProgramming/SimplexSolver.swift index 3edd6b8f..91b58eba 100644 --- a/Sources/BusinessMath/Optimization/LinearProgramming/SimplexSolver.swift +++ b/Sources/BusinessMath/Optimization/LinearProgramming/SimplexSolver.swift @@ -484,7 +484,7 @@ public struct SimplexSolver: Sendable { // print("Variables: \(numVars) original, \(slackCount) slack, \(surplusCount) surplus, \(artificialCount) artificial") // print("Basis: \(basis)") // for (row, constraint) in constraints.enumerated() { - // let rowStr = tableau[row].map { String(format: "%.2f", $0) }.joined(separator: ", ") + // let rowStr = tableau[row].map { $0.number(2) }.joined(separator: ", ") // print("Row \(row): [\(rowStr)] (original: \(constraint.coefficients), rhs: \(constraint.rhs))") // } // } diff --git a/Sources/BusinessMath/Optimization/LinearityValidation.swift b/Sources/BusinessMath/Optimization/LinearityValidation.swift index 3b2a5240..f19c0ce1 100644 --- a/Sources/BusinessMath/Optimization/LinearityValidation.swift +++ b/Sources/BusinessMath/Optimization/LinearityValidation.swift @@ -108,7 +108,7 @@ public func validateLinearModel( if error > tolerance { // Function is nonlinear - construct helpful error message - let pointStr = randomComponents.map { String(format: "%.4f", $0) }.joined(separator: ", ") + let pointStr = randomComponents.map { $0.number(4) }.joined(separator: ", ") let message = """ Function is nonlinear. At point [\(pointStr)]: diff --git a/Sources/BusinessMath/Optimization/PerformanceBenchmark.swift b/Sources/BusinessMath/Optimization/PerformanceBenchmark.swift index 573436ae..655ff943 100644 --- a/Sources/BusinessMath/Optimization/PerformanceBenchmark.swift +++ b/Sources/BusinessMath/Optimization/PerformanceBenchmark.swift @@ -124,8 +124,8 @@ public struct PerformanceBenchmark where V.Scalar == Double { output += "\n" output += "Winner: \(winner.name)\n" - output += " - Fastest average time: \(String(format: "%.4f", winner.avgTime))s\n" - output += " - Success rate: \(String(format: "%.1f", winner.successRate * 100))%\n" + output += " - Fastest average time: \(winner.avgTime.number(4))s\n" + output += " - Success rate: \((winner.successRate * 100).number(1))%\n" return output } @@ -137,19 +137,19 @@ public struct PerformanceBenchmark where V.Scalar == Double { for result in results { output += "\(result.name):\n" - output += " Average time: \(String(format: "%.4f", result.avgTime))s " + - "(± \(String(format: "%.4f", result.stdTime))s)\n" - output += " Average iterations: \(String(format: "%.1f", result.avgIterations))\n" - output += " Success rate: \(String(format: "%.1f", result.successRate * 100))%\n" - output += " Average objective: \(String(format: "%.6f", result.avgObjectiveValue))\n" - output += " Best objective: \(String(format: "%.6f", result.bestObjectiveValue))\n" + output += " Average time: \(result.avgTime.number(4))s " + + "(± \(result.stdTime.number(4))s)\n" + output += " Average iterations: \(result.avgIterations.number(1))\n" + output += " Success rate: \((result.successRate * 100).number(1))%\n" + output += " Average objective: \(result.avgObjectiveValue.number(6))\n" + output += " Best objective: \(result.bestObjectiveValue.number(6))\n" // Show run-by-run details output += " Runs:\n" for (i, run) in result.runs.prefix(5).enumerated() { let num = i + 1 - let time = String(format: "%.4fs", run.executionTime) - let obj = String(format: "%.6f", run.objectiveValue) + let time = "\(run.executionTime.number(4))s" + let obj = run.objectiveValue.number(6) let status = run.converged ? "✓" : "✗" output += " \(num): \(time), \(run.iterations) iter, obj=\(obj) \(status)\n" } diff --git a/Sources/BusinessMath/Performance/CalculationCache.swift b/Sources/BusinessMath/Performance/CalculationCache.swift index 3c9e7893..908c1a17 100644 --- a/Sources/BusinessMath/Performance/CalculationCache.swift +++ b/Sources/BusinessMath/Performance/CalculationCache.swift @@ -727,7 +727,7 @@ extension DataExporter { case .fixed(let amount): builder.append("\(escapeCsv(component.name)),Cost,Fixed,\(amount),\n") case .variable(let percentage): - let percentageStr = String(format: "%.2f%%", percentage * 100) + let percentageStr = "\((percentage * 100).number(2))%" builder.append("\(escapeCsv(component.name)),Cost,Variable,,\(percentageStr)\n") } } diff --git a/Sources/BusinessMath/Statistics/Regression/ModelValidation.swift b/Sources/BusinessMath/Statistics/Regression/ModelValidation.swift index 9ae993a2..88216612 100644 --- a/Sources/BusinessMath/Statistics/Regression/ModelValidation.swift +++ b/Sources/BusinessMath/Statistics/Regression/ModelValidation.swift @@ -334,7 +334,7 @@ public struct ReciprocalParameterRecoveryCheck { var summary = "Parameter Recovery: Multiple Replicates Summary\n" summary += "===============================================\n\n" summary += "Replicates: \(reports.count)\n" - summary += "Passed: \(passCount) (\(String(format: "%.1f%%", passRate * 100)))\n" + summary += "Passed: \(passCount) (\((passRate * 100).number(1))%)\n" summary += "Failed: \(reports.count - passCount)\n\n" // Average errors by parameter @@ -345,7 +345,7 @@ public struct ReciprocalParameterRecoveryCheck { for name in paramNames { let avgRelError = reports.compactMap { $0.relativeErrors[name] } .reduce(T(0), +) / T(reports.count) - summary += "\(name): \(String(format: "%.2f%%", Double(avgRelError) * 100))\n" + summary += "\(name): \((Double(avgRelError) * 100).number(2))%\n" } return summary diff --git a/Sources/BusinessMath/Utilities/Formatting/FloatingPointFormatter.swift b/Sources/BusinessMath/Utilities/Formatting/FloatingPointFormatter.swift index b7298193..c12ba92e 100644 --- a/Sources/BusinessMath/Utilities/Formatting/FloatingPointFormatter.swift +++ b/Sources/BusinessMath/Utilities/Formatting/FloatingPointFormatter.swift @@ -87,6 +87,19 @@ public struct FloatingPointFormatter: Sendable { // MARK: - Private Formatting Methods /// Smart rounding: snap to integer if close, remove trailing zeros + /// Locale-neutral fixed-decimal formatter. Drop-in replacement for the + /// banned C-style format pattern (`%.Nf`). Uses POSIX locale and disables + /// grouping separators to match `printf` semantics exactly. + private func fixedDecimal(_ value: Double, decimals: Int) -> String { + let formatter = NumberFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = decimals + formatter.maximumFractionDigits = decimals + formatter.usesGroupingSeparator = false + return formatter.string(from: NSNumber(value: value)) ?? String(value) + } + private func formatWithSmartRounding(_ value: Double, tolerance: Double) -> String { // Handle edge cases if !value.isFinite { @@ -101,11 +114,11 @@ public struct FloatingPointFormatter: Sendable { // Close to an integer? let nearest = value.rounded() if abs(value - nearest) < tolerance { - return String(format: "%.0f", nearest) + return fixedDecimal(nearest, decimals: 0) } // Otherwise, format with limited decimals and remove trailing zeros - var formatted = String(format: "%.6f", value) + var formatted = fixedDecimal(value, decimals: 6) // Remove trailing zeros if formatted.contains(".") { @@ -136,9 +149,9 @@ public struct FloatingPointFormatter: Sendable { let decimals = max(0, n - Int(magnitude) - 1) if decimals <= 0 { - return String(format: "%.0f", rounded) + return fixedDecimal(rounded, decimals: 0) } else { - var formatted = String(format: "%.\(decimals)f", rounded) + var formatted = fixedDecimal(rounded, decimals: decimals) // Remove trailing zeros while formatted.contains(".") && (formatted.last == "0" || formatted.last == ".") { formatted.removeLast() @@ -166,7 +179,7 @@ public struct FloatingPointFormatter: Sendable { // 2. Check if very close to an integer let nearest = value.rounded() if abs(value - nearest) < tolerance { - return String(format: "%.0f", nearest) + return fixedDecimal(nearest, decimals: 0) } // 3. Use appropriate decimal places based on magnitude @@ -184,7 +197,7 @@ public struct FloatingPointFormatter: Sendable { decimals = 6 } - var formatted = String(format: "%.\(min(decimals, maxDecimals))f", value) + var formatted = fixedDecimal(value, decimals: min(decimals, maxDecimals)) // 4. Remove trailing zeros if formatted.contains(".") { diff --git a/Sources/BusinessMath/Visualization/CommandLineVisualization.swift b/Sources/BusinessMath/Visualization/CommandLineVisualization.swift index a4cfaf1d..c9dc2c83 100644 --- a/Sources/BusinessMath/Visualization/CommandLineVisualization.swift +++ b/Sources/BusinessMath/Visualization/CommandLineVisualization.swift @@ -297,14 +297,6 @@ public func plotTornadoDiagram(_ analysis: TornadoDiagramAnalysis) -> String { // Add value labels underneath (no grouping for alignment) let valueLabels = "\(String(repeating: " ", count: maxInputNameWidth + 3))\(low.number(decimalPlaces, .toNearestOrAwayFromZero, .autoupdatingCurrent, .automatic, .never))\(String(repeating: " ", count: maxBarWidth - 8))\(baseCaseOutput.number(decimalPlaces, .toNearestOrAwayFromZero, .autoupdatingCurrent, .automatic, .never))\(String(repeating: " ", count: maxBarWidth - 8))\(high.number(decimalPlaces, .toNearestOrAwayFromZero, .autoupdatingCurrent, .automatic, .never)))" -// let valueLabels = String(format: "%@%.\(decimalPlaces)f%@%.\(decimalPlaces)f%@%.\(decimalPlaces)f\n", -// String(repeating: " ", count: maxInputNameWidth + 3), -// low, -// String(repeating: " ", count: maxBarWidth - 8), -// baseCaseOutput, -// String(repeating: " ", count: maxBarWidth - 8), -// high -// ) output += valueLabels + "\n" } diff --git a/Tests/BusinessMathTests/Optimization Tests/MultivariateLBFGSTests.swift b/Tests/BusinessMathTests/Optimization Tests/MultivariateLBFGSTests.swift index a66132fe..15a816c2 100644 --- a/Tests/BusinessMathTests/Optimization Tests/MultivariateLBFGSTests.swift +++ b/Tests/BusinessMathTests/Optimization Tests/MultivariateLBFGSTests.swift @@ -173,7 +173,7 @@ struct MultivariateLBFGSTests { // Print comparison print("\nMemory Size Comparison:") for (m, iters, conv, val) in results { - print("m=\(m): iterations=\(iters), converged=\(conv), value=\(String(format: "%.6f", val))") + print("m=\(m): iterations=\(iters), converged=\(conv), value=\(val.number(6))") } // All should converge or make good progress diff --git a/Tests/BusinessMathTests/Portfolio Tests/PortfolioUtilitiesTests.swift b/Tests/BusinessMathTests/Portfolio Tests/PortfolioUtilitiesTests.swift index da5a14e2..83994b7d 100644 --- a/Tests/BusinessMathTests/Portfolio Tests/PortfolioUtilitiesTests.swift +++ b/Tests/BusinessMathTests/Portfolio Tests/PortfolioUtilitiesTests.swift @@ -26,12 +26,32 @@ struct PortfolioUtilitiesTests { #expect(abs(mean - 0.10) < 0.02, "Mean should be near 0.10") } - @Test("Random returns are within reasonable range") + @Test("Random returns are within reasonable range (seeded)") func returnsRange() { - let returns = generateRandomReturns(count: 1000, mean: 0.10, stdDev: 0.05) + // Use a seeded RNG so the test is deterministic and auditable. + // The unseeded `generateRandomReturns` (which uses Double.random + // from the system RNG) was previously the cause of this test + // flaking when an unlucky 5σ+ tail value was drawn — see + // 09_TEST_DRIVEN_DEVELOPMENT.md "Deterministic Randomness Standard". + let rng = SeededRNG(seed: 42) + let count = 1000 + let mean = 0.10 + let stdDev = 0.05 + var returns: [Double] = [] + returns.reserveCapacity(count) + for _ in 0.. -0.10, "Return shouldn't be too negative") #expect(r < 0.30, "Return shouldn't be too high") } diff --git a/Tests/BusinessMathTests/Simulation Tests/GPUPerformanceBenchmark.swift b/Tests/BusinessMathTests/Simulation Tests/GPUPerformanceBenchmark.swift index a524b20c..64e7a131 100644 --- a/Tests/BusinessMathTests/Simulation Tests/GPUPerformanceBenchmark.swift +++ b/Tests/BusinessMathTests/Simulation Tests/GPUPerformanceBenchmark.swift @@ -71,8 +71,12 @@ struct GPUPerformanceBenchmark { let speedup = cpuTime / gpuTime - print(String(format: "%10d | %8.1f | %8.1f | %5.1f×", - iterations, gpuTime, cpuTime, speedup)) + // Manual column padding via String.padding to avoid C-style format string + let iterStr = String(iterations).padding(toLength: 10, withPad: " ", startingAt: 0) + let gpuStr = gpuTime.number(1).padding(toLength: 8, withPad: " ", startingAt: 0) + let cpuStr = cpuTime.number(1).padding(toLength: 8, withPad: " ", startingAt: 0) + let speedStr = speedup.number(1).padding(toLength: 5, withPad: " ", startingAt: 0) + print("\(iterStr) | \(gpuStr) | \(cpuStr) | \(speedStr)×") // Verify GPU was used #expect(gpuResults.usedGPU == true, "GPU should be used for \(iterations) iterations") diff --git a/Tests/BusinessMathTests/Simulation Tests/MonteCarloGPUPerformanceTests.swift b/Tests/BusinessMathTests/Simulation Tests/MonteCarloGPUPerformanceTests.swift index 64e440fb..d76cda3b 100644 --- a/Tests/BusinessMathTests/Simulation Tests/MonteCarloGPUPerformanceTests.swift +++ b/Tests/BusinessMathTests/Simulation Tests/MonteCarloGPUPerformanceTests.swift @@ -108,7 +108,7 @@ struct MonteCarloGPUPerformanceTests { print("📊 10K Iterations Benchmark:") print(" CPU: \(Int(cpuTime * 1000))ms (usedGPU: \(cpuResults.usedGPU))") print(" GPU: \(Int(gpuTime * 1000))ms (usedGPU: \(gpuResults.usedGPU))") - print(" Speedup: \(String(format: "%.1f", speedup))x") + print(" Speedup: \(speedup.number(1))x") print(" Expected: 5-10x speedup on M1/M2/M3") #endif } @@ -165,9 +165,9 @@ struct MonteCarloGPUPerformanceTests { let speedup = cpuTime / gpuTime print("📊 100K Iterations Benchmark:") - print(" CPU: \(String(format: "%.2f", cpuTime))s (usedGPU: \(cpuResults.usedGPU))") - print(" GPU: \(String(format: "%.2f", gpuTime))s (usedGPU: \(gpuResults.usedGPU))") - print(" Speedup: \(String(format: "%.1f", speedup))x") + print(" CPU: \(cpuTime.number(2))s (usedGPU: \(cpuResults.usedGPU))") + print(" GPU: \(gpuTime.number(2))s (usedGPU: \(gpuResults.usedGPU))") + print(" Speedup: \(speedup.number(1))x") print(" Expected: 3-8x speedup for complex models on Apple Silicon") print(" Results match: \(abs(cpuResults.statistics.mean - gpuResults.statistics.mean) / cpuResults.statistics.mean < 0.01 ? "✓" : "✗")") #endif @@ -218,9 +218,9 @@ struct MonteCarloGPUPerformanceTests { let speedup = cpuTime / gpuTime print("📊 1M Iterations Benchmark:") - print(" CPU: \(String(format: "%.1f", cpuTime))s (usedGPU: \(cpuResults.usedGPU))") - print(" GPU: \(String(format: "%.2f", gpuTime))s (usedGPU: \(gpuResults.usedGPU))") - print(" Speedup: \(String(format: "%.1f", speedup))x") + print(" CPU: \(cpuTime.number(1))s (usedGPU: \(cpuResults.usedGPU))") + print(" GPU: \(gpuTime.number(2))s (usedGPU: \(gpuResults.usedGPU))") + print(" Speedup: \(speedup.number(1))x") print(" Expected: 50-100x speedup on M1/M2/M3") print(" Results match: \(abs(cpuResults.statistics.mean - gpuResults.statistics.mean) / cpuResults.statistics.mean < 0.01 ? "✓" : "✗")") #endif @@ -273,7 +273,7 @@ struct MonteCarloGPUPerformanceTests { print("📊 Model Complexity Benchmark (\(iterations) iterations):") print(" Simple (a + b): \(Int(simpleTime * 1000))ms") print(" Complex ((a*b) + (c*d) - (e/2)): \(Int(complexTime * 1000))ms") - print(" Overhead: \(String(format: "%.1f", (complexTime / simpleTime - 1) * 100))%") + print(" Overhead: \(((complexTime / simpleTime - 1) * 100).number(1))%") print(" Expected: Minimal overhead due to parallel GPU execution") #endif } diff --git a/Tests/BusinessMathTests/Statistics Tests/Regression Tests/MatrixBackendBenchmarks.swift b/Tests/BusinessMathTests/Statistics Tests/Regression Tests/MatrixBackendBenchmarks.swift index 00f67bbe..6a9b364d 100644 --- a/Tests/BusinessMathTests/Statistics Tests/Regression Tests/MatrixBackendBenchmarks.swift +++ b/Tests/BusinessMathTests/Statistics Tests/Regression Tests/MatrixBackendBenchmarks.swift @@ -66,7 +66,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("CPU (100×100): \(String(format: "%.3f", time * 1000))ms") + print("CPU (100×100): \((time * 1000).number(3))ms") } #if canImport(Accelerate) @@ -81,7 +81,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("Accelerate (100×100): \(String(format: "%.3f", time * 1000))ms") + print("Accelerate (100×100): \((time * 1000).number(3))ms") } #endif @@ -100,7 +100,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("Metal (100×100): \(String(format: "%.3f", time * 1000))ms") + print("Metal (100×100): \((time * 1000).number(3))ms") } #endif @@ -117,7 +117,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("CPU (500×500): \(String(format: "%.3f", time * 1000))ms") + print("CPU (500×500): \((time * 1000).number(3))ms") } #if canImport(Accelerate) @@ -132,7 +132,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("Accelerate (500×500): \(String(format: "%.3f", time * 1000))ms") + print("Accelerate (500×500): \((time * 1000).number(3))ms") } #endif @@ -151,7 +151,7 @@ struct MatrixBackendBenchmarks { _ = try backend.multiply(A, B) } - print("Metal (500×500): \(String(format: "%.3f", time * 1000))ms") + print("Metal (500×500): \((time * 1000).number(3))ms") } #endif diff --git a/Tests/BusinessMathTests/StreamingTests/StreamingFrequencyDomainTests.swift b/Tests/BusinessMathTests/StreamingTests/StreamingFrequencyDomainTests.swift index 8de39de9..eab9862c 100644 --- a/Tests/BusinessMathTests/StreamingTests/StreamingFrequencyDomainTests.swift +++ b/Tests/BusinessMathTests/StreamingTests/StreamingFrequencyDomainTests.swift @@ -192,7 +192,7 @@ struct StreamingFrequencyDomainTests { // MARK: - AccelerateFFTBackend Tests (conditional) #if canImport(Accelerate) - @Test("Accelerate FFT: matches Pure Swift within tolerance") + @Test("Accelerate FFT: matches Pure Swift bin-for-bin (tightened in v2.1.3)") func accelerateMatchesPureSwift() { let pureBackend = PureSwiftFFTBackend() let accelBackend = AccelerateFFTBackend() @@ -209,15 +209,21 @@ struct StreamingFrequencyDomainTests { let pureSpectrum = pureBackend.powerSpectrum(signal) let accelSpectrum = accelBackend.powerSpectrum(signal) + // Tightened in v2.1.3: assert ABSOLUTE bin-for-bin equivalence, not + // just peak location. The previous version only checked peak bin + // index, which let the v2.1.0 4× scaling bug slip through (the + // peak was at the right bin, just 4× too large). The PSD work in + // v2.1.1 fixed AccelerateFFTBackend to apply a ×0.25 scaling + // correction; this test locks that fix in by requiring exact + // bin-by-bin agreement at machine precision. #expect(pureSpectrum.count == accelSpectrum.count) - - // Guard against invalid spectrum (prevents Range crash if count < 2) - guard pureSpectrum.count > 1, accelSpectrum.count > 1 else { return } - - // Both should agree on peak location - let purePeak = (1.. 0.5) - #expect(ratio < 2.0) + #expect(abs(ratio - 1.0) < 1e-12, "Parseval ratio off: \(ratio)") } // MARK: - Numerical Stability Tests