From cb926fc07e3cea9215997a83d1ce7b81e708bd44 Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 1 Jul 2026 00:52:46 -0400 Subject: [PATCH 1/2] algorithm: add double rectmaxvol Why: - Continue the pre-1.0 MaxVol algorithm surface with the rectangular Double variant. - Defer Swift Package Index submission until v1.0.0 in the roadmap. Verification: - swift build - swift test - swift package dump-package - swift build --target MaxVol -Xswiftc -emit-symbol-graph -Xswiftc -emit-symbol-graph-dir -Xswiftc .build/symbol-graphs - xcrun docc convert Sources/MaxVol/MaxVol.docc --fallback-display-name MaxVol --fallback-bundle-identifier com.gaelic-ghost.MaxVol --fallback-bundle-version 0.5.0 --additional-symbol-graph-dir .build/symbol-graphs --output-path .build/docc/MaxVol.doccarchive - scripts/repo-maintenance/validate-all.sh - swiftformat Sources Tests --lint --- ROADMAP.md | 18 +- Sources/MaxVol/MaxVol.docc/MaxVol.md | 13 + .../MaxVol.docc/ToleranceAndConvergence.md | 70 ++++ Sources/MaxVol/MaxVolError.swift | 15 +- Sources/MaxVol/MaxVolOptions.swift | 1 - Sources/MaxVol/MaxVolResult.swift | 4 +- Sources/MaxVol/RectMaxVol.swift | 163 +++++++++ Sources/MaxVol/RectMaxVolOptions.swift | 77 +++++ Tests/MaxVolTests/RectMaxVolTests.swift | 319 ++++++++++++++++++ 9 files changed, 666 insertions(+), 14 deletions(-) create mode 100644 Sources/MaxVol/MaxVol.docc/ToleranceAndConvergence.md create mode 100644 Sources/MaxVol/RectMaxVol.swift create mode 100644 Sources/MaxVol/RectMaxVolOptions.swift create mode 100644 Tests/MaxVolTests/RectMaxVolTests.swift diff --git a/ROADMAP.md b/ROADMAP.md index 6eacf8d..e6216c2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,8 +2,8 @@ ## Current Focus -- Ship `v0.5.0` with square MaxVol for real-valued `Double` matrices backed by - modern Accelerate BLAS/LAPACK. +- Build from `v0.5.0` toward `v1.0.0` with real-valued `Double` algorithms + backed by modern Accelerate BLAS/LAPACK. - Keep Swift Testing as the package test surface and require reference fixtures before broadening the algorithm surface. - Treat public API compatibility as pre-1.0 until RectMaxVol, Float support, and @@ -66,22 +66,24 @@ ## Swift Package Index +- Defer Swift Package Index submission until `v1.0.0`. - Keep SPI readiness checks in the release path. - Keep `.spi.yml` aligned with DocC targets when package documentation changes. - Verify `swift package dump-package`, `swift build`, `swift test`, and DocC - generation before submitting to Swift Package Index. -- Submit only after the GitHub repository is public and a SemVer tag exists. + generation before the eventual Swift Package Index submission. +- Submit only after the GitHub repository is public and the `v1.0.0` SemVer tag + exists. ## GitHub Publication - Keep the public `gaelic-ghost/MaxVol` repository aligned with SemVer tags. - Merge focused pull requests into `main` only after serial `swift build`, `swift test`, repo-maintenance validation, and DocC conversion pass. -- Submit to Swift Package Index after the first public SemVer tag is pushed. +- Do not submit to Swift Package Index before `v1.0.0`. ## Before `1.0.0` -- Implement and test RectMaxVol for `Double`. +- Complete RectMaxVol for `Double` with reference-parity fixtures. - Add `Float` support with the same API shape and reference fixtures. - Decide whether complex-valued matrices are in scope for `1.0.0` or explicitly post-1.0. @@ -91,5 +93,5 @@ - Add performance benchmarks for allocation count and row-swap throughput. - Expand DocC with algorithm notes, limitations, and reference-fixture provenance. -- Submit the tagged public package to Swift Package Index and verify rendered - documentation. +- Submit the tagged `v1.0.0` public package to Swift Package Index and verify + rendered documentation. diff --git a/Sources/MaxVol/MaxVol.docc/MaxVol.md b/Sources/MaxVol/MaxVol.docc/MaxVol.md index fd0d352..672d47a 100644 --- a/Sources/MaxVol/MaxVol.docc/MaxVol.md +++ b/Sources/MaxVol/MaxVol.docc/MaxVol.md @@ -44,6 +44,13 @@ rows: let coefficients = result.coefficients ``` +Call ``rectMaxVol(_:options:)`` when the basis may contain more rows than the +matrix column count: + +```swift +let rectangular = try rectMaxVol(matrix, options: RectMaxVolOptions(minRows: 3)) +``` + ## Topics ### Matrix Storage @@ -53,6 +60,12 @@ let coefficients = result.coefficients ### MaxVol - ``maxVol(_:options:)`` +- ``rectMaxVol(_:options:)`` - ``MaxVolOptions`` +- ``RectMaxVolOptions`` - ``MaxVolResult`` - ``MaxVolError`` + +### Algorithm Notes + +- diff --git a/Sources/MaxVol/MaxVol.docc/ToleranceAndConvergence.md b/Sources/MaxVol/MaxVol.docc/ToleranceAndConvergence.md new file mode 100644 index 0000000..6f2a527 --- /dev/null +++ b/Sources/MaxVol/MaxVol.docc/ToleranceAndConvergence.md @@ -0,0 +1,70 @@ +# Tolerance and Convergence + +Use tolerance, iteration limits, and row-count bounds to decide how much work +MaxVol should do before returning a basis. + +## Overview + +MaxVol returns a ``MaxVolResult`` instead of only returning selected row indices +so callers can inspect the stopping behavior. The ``MaxVolResult/converged`` +flag reports whether the configured stopping criterion was satisfied, while +``MaxVolResult/iterations`` reports how many update steps ran after the initial +basis was chosen. + +For an input matrix `A`, both square and rectangular results use the same +reconstruction shape: + +```swift +// A ~= C * A[selectedRows, :] +let selectedRows = result.selectedRows +let coefficients = result.coefficients +``` + +## Square MaxVol + +``maxVol(_:options:)`` selects exactly one row per matrix column. The square +algorithm starts from an LU-pivoted basis, computes expansion coefficients, and +swaps rows while any coefficient magnitude is larger than +``MaxVolOptions/tolerance``. + +The square tolerance must be at least `1.0`. Values closer to `1.0` usually do +more row swaps and produce a stronger local maximum-volume basis. Larger values +allow earlier stopping. + +If ``MaxVolOptions/maxIterations`` is reached first, the result is still +validated and reconstructs through its current selected rows, but +``MaxVolResult/converged`` is `false`. + +## RectMaxVol + +``rectMaxVol(_:options:)`` starts from square MaxVol, then appends extra rows. +The rectangular stopping test uses coefficient row norms instead of individual +coefficient magnitudes: unselected rows are appended while their coefficient row +norm exceeds ``RectMaxVolOptions/tolerance``. + +``RectMaxVolOptions/minRows`` can force extra rows even when the tolerance is +already satisfied. ``RectMaxVolOptions/maxRows`` can stop the append loop before +the tolerance is satisfied; in that case ``MaxVolResult/converged`` is `false`. +When every row is selected, the result is considered converged because there are +no unselected coefficient rows left to violate the tolerance. + +## Choosing Options + +Use the default options first for general row-basis selection. Tighten square +``MaxVolOptions/tolerance`` or raise ``MaxVolOptions/maxIterations`` when a +stronger square basis matters. For rectangular selection, set +``RectMaxVolOptions/minRows`` when a downstream approximation requires a minimum +basis size, and set ``RectMaxVolOptions/maxRows`` when runtime or storage must +be bounded. + +## Topics + +### Related APIs + +- ``maxVol(_:options:)`` +- ``rectMaxVol(_:options:)`` +- ``MaxVolOptions`` +- ``RectMaxVolOptions`` +- ``MaxVolResult`` +- ``MaxVolResult/converged`` +- ``MaxVolResult/iterations`` diff --git a/Sources/MaxVol/MaxVolError.swift b/Sources/MaxVol/MaxVolError.swift index 7486b7f..49cce1d 100644 --- a/Sources/MaxVol/MaxVolError.swift +++ b/Sources/MaxVol/MaxVolError.swift @@ -21,6 +21,14 @@ public enum MaxVolError: Error, Equatable, Sendable { /// A requested selection count is incompatible with the available rows. case invalidSelectionCount(requested: Int, availableRows: Int) + /// Rectangular row bounds are incompatible with the input matrix. + case invalidRowSelectionBounds( + minRows: Int, + maxRows: Int, + requiredRows: Int, + availableRows: Int + ) + /// A selected row index is outside the input matrix bounds. case invalidSelectedRowIndex(row: Int, availableRows: Int) @@ -30,7 +38,7 @@ public enum MaxVolError: Error, Equatable, Sendable { /// The result coefficient matrix does not match the selected row count. case coefficientColumnMismatch(selectedRows: Int, coefficientColumns: Int) - /// The convergence tolerance is not finite or is less than `1.0`. + /// The convergence tolerance is outside the valid range for the requested algorithm. case invalidTolerance(Double) /// The maximum iteration limit is negative. @@ -44,7 +52,6 @@ public enum MaxVolError: Error, Equatable, Sendable { /// An Accelerate LAPACK routine reported an unexpected nonzero `info` value. case lapackFailure(routine: String, info: Int) - } extension MaxVolError: CustomStringConvertible { @@ -64,6 +71,8 @@ extension MaxVolError: CustomStringConvertible { "MaxVol requires a tall or square input matrix with rows >= columns, but received rows: \(rows), columns: \(columns)." case let .invalidSelectionCount(requested, availableRows): "MaxVol cannot select \(requested) rows from a matrix with \(availableRows) available rows." + case let .invalidRowSelectionBounds(minRows, maxRows, requiredRows, availableRows): + "RectMaxVol row bounds are invalid: minRows \(minRows), maxRows \(maxRows), required rows at least \(requiredRows), available rows \(availableRows)." case let .invalidSelectedRowIndex(row, availableRows): "MaxVol selected row index \(row) is out of bounds for a matrix with \(availableRows) rows." case let .duplicateSelectedRow(row): @@ -71,7 +80,7 @@ extension MaxVolError: CustomStringConvertible { case let .coefficientColumnMismatch(selectedRows, coefficientColumns): "MaxVol result has \(selectedRows) selected rows but \(coefficientColumns) coefficient columns." case let .invalidTolerance(tolerance): - "MaxVol tolerance must be finite and at least 1.0, but received \(tolerance)." + "MaxVol tolerance is outside the valid range for the requested algorithm, but received \(tolerance)." case let .invalidIterationLimit(limit): "MaxVol maximum iteration limit must be nonnegative, but received \(limit)." case let .invalidIterationCount(iterations): diff --git a/Sources/MaxVol/MaxVolOptions.swift b/Sources/MaxVol/MaxVolOptions.swift index f86e72c..7eec241 100644 --- a/Sources/MaxVol/MaxVolOptions.swift +++ b/Sources/MaxVol/MaxVolOptions.swift @@ -20,7 +20,6 @@ public struct MaxVolOptions: Equatable, Hashable, Sendable { guard tolerance.isFinite, tolerance >= 1 else { throw MaxVolError.invalidTolerance(tolerance) } - guard maxIterations >= 0 else { throw MaxVolError.invalidIterationLimit(maxIterations) } diff --git a/Sources/MaxVol/MaxVolResult.swift b/Sources/MaxVol/MaxVolResult.swift index 1994cde..fd8c482 100644 --- a/Sources/MaxVol/MaxVolResult.swift +++ b/Sources/MaxVol/MaxVolResult.swift @@ -9,10 +9,10 @@ public struct MaxVolResult: Sendable { /// `A ~= coefficients * A[selectedRows, :]`. public let coefficients: DenseColumnMajorMatrix - /// The number of row-replacement iterations performed after the initial basis. + /// The number of algorithm iterations performed after the initial basis. public let iterations: Int - /// Whether the coefficient matrix satisfied the configured tolerance. + /// Whether the coefficient matrix satisfied the configured stopping criterion. public let converged: Bool /// Creates a validated result value. diff --git a/Sources/MaxVol/RectMaxVol.swift b/Sources/MaxVol/RectMaxVol.swift new file mode 100644 index 0000000..2c233f5 --- /dev/null +++ b/Sources/MaxVol/RectMaxVol.swift @@ -0,0 +1,163 @@ +import Accelerate + +private typealias LAPACKInt = __LAPACK_int + +/// Selects a high-volume rectangular row basis from a tall dense `Double` matrix. +/// +/// RectMaxVol starts with ``maxVol(_:options:)`` and appends rows until every +/// remaining unselected coefficient row satisfies ``RectMaxVolOptions/tolerance`` +/// or the configured row bounds stop the append loop. +public func rectMaxVol( + _ matrix: DenseColumnMajorMatrix, + options: RectMaxVolOptions = RectMaxVolOptions() +) throws -> MaxVolResult { + let input = try matrix.validatedTallMatrix() + let options = try options.resolved(for: input) + let initial = try maxVol( + input, + options: MaxVolOptions(maxIterations: options.startMaxVolIterations) + ) + var selectedRows = initial.selectedRows + var coefficients = initial.coefficients + var iterations = 0 + + while true { + let candidate = maximumUnselectedRowNormSquared( + in: coefficients, + selectedRows: selectedRows + ) + let toleranceNeedsAppend = candidate.value > options.toleranceSquared + && selectedRows.count < options.maxRows + let minimumNeedsAppend = selectedRows.count < options.minRows + + guard toleranceNeedsAppend || minimumNeedsAppend else { + let output = try coefficientsWithIdentityRows( + coefficients, + selectedRows: selectedRows + ) + return try MaxVolResult( + selectedRows: selectedRows, + coefficients: output, + iterations: iterations, + converged: candidate.value <= options.toleranceSquared || selectedRows.count == input.rows + ) + } + guard let candidateRow = candidate.row, selectedRows.count < options.maxRows else { + let output = try coefficientsWithIdentityRows( + coefficients, + selectedRows: selectedRows + ) + return try MaxVolResult( + selectedRows: selectedRows, + coefficients: output, + iterations: iterations, + converged: false + ) + } + + coefficients = try appendRectangularBasisRow( + candidateRow, + to: coefficients + ) + selectedRows.append(candidateRow) + iterations += 1 + } +} + +private struct RowNormCandidate { + let row: Int? + let value: Double +} + +private func maximumUnselectedRowNormSquared( + in coefficients: DenseColumnMajorMatrix, + selectedRows: [Int] +) -> RowNormCandidate { + let selected = Set(selectedRows) + var candidate = RowNormCandidate(row: nil, value: 0) + + for row in 0.. candidate.value { + candidate = RowNormCandidate(row: row, value: value) + } + } + + return candidate +} + +private func appendRectangularBasisRow( + _ candidateRow: Int, + to coefficients: DenseColumnMajorMatrix +) throws -> DenseColumnMajorMatrix { + let candidateCoefficients = try coefficients.row(candidateRow) + let projection = (0.. Void in + candidateCoefficients.withUnsafeBufferPointer { coefficientBuffer -> Void in + updatedValues.withUnsafeMutableBufferPointer { updatedBuffer -> Void in + cblas_dger( + CblasColMajor, + rowCount, + columnCount, + -scale, + projectionBuffer.baseAddress, + increment, + coefficientBuffer.baseAddress, + increment, + updatedBuffer.baseAddress, + leadingDimension + ) + } + } + } + + updatedValues.append(contentsOf: appendedColumn) + return try DenseColumnMajorMatrix( + rows: coefficients.rows, + columns: coefficients.columns + 1, + columnMajorValues: updatedValues + ) +} + +private func coefficientsWithIdentityRows( + _ coefficients: DenseColumnMajorMatrix, + selectedRows: [Int] +) throws -> DenseColumnMajorMatrix { + var output = coefficients + + for (identityColumn, selectedRow) in selectedRows.enumerated() { + for column in 0.. LAPACKInt { + guard value <= Int(LAPACKInt.max) else { + throw MaxVolError.invalidDimensions(rows: value, columns: value) + } + + return LAPACKInt(value) +} diff --git a/Sources/MaxVol/RectMaxVolOptions.swift b/Sources/MaxVol/RectMaxVolOptions.swift new file mode 100644 index 0000000..729b855 --- /dev/null +++ b/Sources/MaxVol/RectMaxVolOptions.swift @@ -0,0 +1,77 @@ +/// Controls rectangular MaxVol row appends. +public struct RectMaxVolOptions: Equatable, Hashable, Sendable { + /// The maximum allowed norm for unselected coefficient rows. + /// + /// Values must be finite and positive. The default follows the common + /// rectangular MaxVol threshold. + public let tolerance: Double + + /// The minimum number of rows to select. + /// + /// `nil` uses the input matrix column count. + public let minRows: Int? + + /// The maximum number of rows to select. + /// + /// `nil` allows selecting every input row. + public let maxRows: Int? + + /// The number of square MaxVol row-replacement iterations used for the initial basis. + public let startMaxVolIterations: Int + + /// Creates RectMaxVol options. + public init( + tolerance: Double = 1.0, + minRows: Int? = nil, + maxRows: Int? = nil, + startMaxVolIterations: Int = 10 + ) { + self.tolerance = tolerance + self.minRows = minRows + self.maxRows = maxRows + self.startMaxVolIterations = startMaxVolIterations + } + + func resolved(for matrix: DenseColumnMajorMatrix) throws -> ResolvedRectMaxVolOptions { + guard tolerance.isFinite, tolerance > 0 else { + throw MaxVolError.invalidTolerance(tolerance) + } + guard startMaxVolIterations >= 0 else { + throw MaxVolError.invalidIterationLimit(startMaxVolIterations) + } + + let requiredRows = matrix.columns + let resolvedMinRows = minRows ?? requiredRows + let resolvedMaxRows = maxRows ?? matrix.rows + + guard + resolvedMinRows >= requiredRows, + resolvedMaxRows >= requiredRows, + resolvedMinRows <= resolvedMaxRows, + resolvedMaxRows <= matrix.rows + else { + throw MaxVolError.invalidRowSelectionBounds( + minRows: resolvedMinRows, + maxRows: resolvedMaxRows, + requiredRows: requiredRows, + availableRows: matrix.rows + ) + } + + return ResolvedRectMaxVolOptions( + tolerance: tolerance, + minRows: resolvedMinRows, + maxRows: resolvedMaxRows, + startMaxVolIterations: startMaxVolIterations + ) + } +} + +struct ResolvedRectMaxVolOptions: Equatable, Hashable { + let tolerance: Double + let minRows: Int + let maxRows: Int + let startMaxVolIterations: Int + + var toleranceSquared: Double { tolerance * tolerance } +} diff --git a/Tests/MaxVolTests/RectMaxVolTests.swift b/Tests/MaxVolTests/RectMaxVolTests.swift new file mode 100644 index 0000000..bfe5b9f --- /dev/null +++ b/Tests/MaxVolTests/RectMaxVolTests.swift @@ -0,0 +1,319 @@ +@testable import MaxVol +import Testing + +struct RectMaxVolTests { + // Reference fixture values in this suite were generated with + // maxvolpy.maxvol.py_rect_maxvol running on NumPy/SciPy. + + @Test func maxVolPyReferenceFixtureAppendsRequiredMinimumRows() throws { + let matrix = try DenseColumnMajorMatrix( + rows: 4, + columns: 2, + rowMajorValues: [ + 1.0, 0.0, + 0.0, 1.0, + 0.5, 0.25, + -0.25, 0.75, + ] + ) + + let result = try rectMaxVol(matrix, options: RectMaxVolOptions(minRows: 3)) + + #expect(result.selectedRows == [0, 1, 3]) + #expect(result.iterations == 1) + #expect(result.converged) + try expectCoefficients( + result.coefficients, + rowMajorValues: [ + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.509_615_384_615_384_6, 0.221_153_846_153_846_15, 0.038_461_538_461_538_464, + 0.0, 0.0, 1.0, + ] + ) + try expectReconstruction(of: matrix, using: result) + try expectSelectedRowsAreIdentity(result) + } + + @Test func maxVolPyReferenceFixtureRespectsMaximumRowsLimit() throws { + let matrix = try DenseColumnMajorMatrix( + rows: 3, + columns: 2, + rowMajorValues: [ + 0.25, 0.0, + 0.5, 1.0, + 0.5, 1.5, + ] + ) + + let result = try rectMaxVol(matrix, options: RectMaxVolOptions(tolerance: 0.9, maxRows: 2)) + + #expect(result.selectedRows == [2, 0]) + #expect(result.iterations == 0) + #expect(!result.converged) + try expectCoefficients( + result.coefficients, + rowMajorValues: [ + 0.0, 1.0, + 2.0 / 3.0, 2.0 / 3.0, + 1.0, 0.0, + ] + ) + try expectReconstruction(of: matrix, using: result) + #expect(maximumUnselectedRowNorm(in: result) > 0.9) + } + + @Test func maxVolPyReferenceFixtureSelectsAllRowsWhenNeeded() throws { + let matrix = try DenseColumnMajorMatrix( + rows: 3, + columns: 2, + rowMajorValues: [ + 0.25, 0.0, + 0.5, 1.0, + 0.5, 1.5, + ] + ) + + let result = try rectMaxVol(matrix, options: RectMaxVolOptions(tolerance: 0.9, maxRows: 3)) + + #expect(result.selectedRows == [2, 0, 1]) + #expect(result.iterations == 1) + #expect(result.converged) + try expectCoefficients( + result.coefficients, + rowMajorValues: [ + 0.0, 1.0, 0.0, + 0.0, 0.0, 1.0, + 1.0, 0.0, 0.0, + ] + ) + try expectReconstruction(of: matrix, using: result) + try expectSelectedRowsAreIdentity(result) + } + + @Test func maxVolPyReferenceFixtureMatchesAppendCase() throws { + let matrix = try DenseColumnMajorMatrix( + rows: 5, + columns: 2, + rowMajorValues: [ + 1.0, 0.0, + 0.0, 1.0, + 1.2, 0.2, + 0.1, 1.3, + 0.8, 0.8, + ] + ) + + let result = try rectMaxVol(matrix, options: RectMaxVolOptions(minRows: 3)) + + #expect(result.selectedRows == [2, 3, 0]) + #expect(result.iterations == 1) + #expect(result.converged) + try expectCoefficients( + result.coefficients, + rowMajorValues: [ + 0.0, 0.0, 1.0, + 0.011_215_135_556_855_861, 0.767_505_363_760_483_6, -0.090_208_699_044_275_4, + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.399_453_871_659_840_1, 0.553_930_173_590_793_9, 0.265_262_336_649_112_6, + ] + ) + try expectReconstruction(of: matrix, using: result) + try expectSelectedRowsAreIdentity(result) + #expect(maximumUnselectedRowNorm(in: result) <= 1.0) + } + + @Test func invalidRectangularOptionsAreRejected() throws { + let matrix = try DenseColumnMajorMatrix( + rows: 4, + columns: 2, + rowMajorValues: [ + 1.0, 0.0, + 0.0, 1.0, + 0.5, 0.25, + -0.25, 0.75, + ] + ) + + #expect(throws: MaxVolError.invalidTolerance(0.0)) { + try rectMaxVol(matrix, options: RectMaxVolOptions(tolerance: 0.0)) + } + do { + _ = try rectMaxVol(matrix, options: RectMaxVolOptions(tolerance: .nan)) + Issue.record("RectMaxVol accepted NaN tolerance.") + } catch let MaxVolError.invalidTolerance(tolerance) { + #expect(tolerance.isNaN) + } catch { + Issue.record("RectMaxVol threw unexpected error for NaN tolerance: \(error).") + } + #expect(throws: MaxVolError.invalidIterationLimit(-1)) { + try rectMaxVol(matrix, options: RectMaxVolOptions(startMaxVolIterations: -1)) + } + #expect( + throws: MaxVolError.invalidRowSelectionBounds( + minRows: 1, + maxRows: 4, + requiredRows: 2, + availableRows: 4 + ) + ) { + try rectMaxVol(matrix, options: RectMaxVolOptions(minRows: 1)) + } + #expect( + throws: MaxVolError.invalidRowSelectionBounds( + minRows: 3, + maxRows: 2, + requiredRows: 2, + availableRows: 4 + ) + ) { + try rectMaxVol(matrix, options: RectMaxVolOptions(minRows: 3, maxRows: 2)) + } + #expect( + throws: MaxVolError.invalidRowSelectionBounds( + minRows: 2, + maxRows: 5, + requiredRows: 2, + availableRows: 4 + ) + ) { + try rectMaxVol(matrix, options: RectMaxVolOptions(maxRows: 5)) + } + } + + @Test func randomizedOrthonormalTallMatrixSatisfiesSquareAndRectangularCriteria() throws { + let matrix = try orthonormalColumns(rows: 12, columns: 4, seed: 0xC0FFEE) + + let square = try maxVol(matrix, options: MaxVolOptions(tolerance: 1.05, maxIterations: 100)) + #expect(square.selectedRows.count == matrix.columns) + #expect(square.converged) + #expect(maximumAbsoluteCoefficient(in: square.coefficients) <= 1.05) + try expectReconstruction(of: matrix, using: square, tolerance: 1e-9) + + let rectangular = try rectMaxVol(matrix, options: RectMaxVolOptions(tolerance: 1.0)) + #expect(rectangular.selectedRows.count >= matrix.columns) + #expect(rectangular.converged) + #expect(maximumUnselectedRowNorm(in: rectangular) <= 1.0 + 1e-12) + try expectReconstruction(of: matrix, using: rectangular, tolerance: 1e-9) + try expectSelectedRowsAreIdentity(rectangular) + } +} + +private func expectReconstruction( + of matrix: DenseColumnMajorMatrix, + using result: MaxVolResult, + tolerance: Double = 1e-10 +) throws { + for row in 0.., + rowMajorValues: [Double], + tolerance: Double = 1e-12 +) throws { + let expected = try DenseColumnMajorMatrix( + rows: coefficients.rows, + columns: coefficients.columns, + rowMajorValues: rowMajorValues + ) + + for row in 0..) throws { + for (identityColumn, selectedRow) in result.selectedRows.enumerated() { + for column in 0.. +) -> Double { + coefficients.values.map(abs).max() ?? 0 +} + +private func maximumUnselectedRowNorm(in result: MaxVolResult) -> Double { + let selected = Set(result.selectedRows) + return (0.. DenseColumnMajorMatrix { + var generator = SeededGenerator(state: seed) + var columnVectors = (0.. [Double] in + (0.. Double { + zip(left, right).reduce(0) { total, pair in + total + pair.0 * pair.1 + } +} + +private struct SeededGenerator { + var state: UInt64 + + mutating func nextDouble() -> Double { + state = state &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + let scaled = Double(state >> 11) / Double(UInt64.max >> 11) + return scaled * 2 - 1 + } +} From 5707b291df114a3be72b325e39f6f46946538ee7 Mon Sep 17 00:00:00 2001 From: Gale W Date: Wed, 1 Jul 2026 00:57:47 -0400 Subject: [PATCH 2/2] docs: update spi roadmap status --- ROADMAP.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e6216c2..a4a3a72 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -66,20 +66,18 @@ ## Swift Package Index -- Defer Swift Package Index submission until `v1.0.0`. +- Swift Package Index submission has been started for the public package. - Keep SPI readiness checks in the release path. - Keep `.spi.yml` aligned with DocC targets when package documentation changes. - Verify `swift package dump-package`, `swift build`, `swift test`, and DocC - generation before the eventual Swift Package Index submission. -- Submit only after the GitHub repository is public and the `v1.0.0` SemVer tag - exists. + generation before each tagged release. +- Monitor SPI package ingestion and rendered documentation as release tags land. ## GitHub Publication - Keep the public `gaelic-ghost/MaxVol` repository aligned with SemVer tags. - Merge focused pull requests into `main` only after serial `swift build`, `swift test`, repo-maintenance validation, and DocC conversion pass. -- Do not submit to Swift Package Index before `v1.0.0`. ## Before `1.0.0` @@ -93,5 +91,4 @@ - Add performance benchmarks for allocation count and row-swap throughput. - Expand DocC with algorithm notes, limitations, and reference-fixture provenance. -- Submit the tagged `v1.0.0` public package to Swift Package Index and verify - rendered documentation. +- Verify Swift Package Index renders the tagged `v1.0.0` documentation cleanly.