Skip to content

Commit b8f2ee8

Browse files
Add cs_cholesky wrapper and drop module-level docstring (#1)
Wraps the third CXSparse direct factorization, cs_*_schol + cs_*_chol, for all four (Tv, Ti) ∈ {Float64, ComplexF64} × {Int32, Int64} combos, exported as `cs_cholesky` returning a `CSCholesky`. Solve via `\` / `ldiv!`; finalizer frees the symbolic + numeric C-side structs as with CSQR / CSLU. The solve path is the textbook `x = P' * (L' \ (L \ (P * b)))`, using new `_cs_ltsolve!` / `_cs_pvec!` ccall shims and the existing `_cs_lsolve!` / `_cs_ipvec!` ones. Cholesky tests cover real SPD and complex Hermitian PD inputs at n ∈ {5, 25, 100} over both index types, plus rejects-non-square, rejects-non-PD (cs_chol returns NULL on the first non-positive pivot), and dimension-mismatch checks. The finalizer GC stress test now exercises the Cholesky path too. Also drops the module-level docstring on `CXSparse`; the README is now the single source for the overview, and the README has been updated to reflect the new solver, add a Cholesky quick-start example, and add a CHOLMOD column to the comparison table. Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
1 parent 8a50d52 commit b8f2ee8

3 files changed

Lines changed: 233 additions & 54 deletions

File tree

README.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,48 @@
22

33
Julia wrapper around the [CXSparse](https://github.com/DrTimothyAldenDavis/SuiteSparse/tree/dev/CXSparse) library from SuiteSparse — the lightweight, textbook-style sparse direct solvers from Tim Davis's *Direct Methods for Sparse Linear Systems* (CSparse), extended to support `ComplexF64` values and both 32- and 64-bit indices.
44

5-
CXSparse is the QR / LU counterpart to **KLU's design philosophy**: a small symbolic phase, very little per-call overhead, well-suited to small-to-medium sparse problems (n up to a few thousand). It complements the multifrontal solvers (UMFPACK, SPQR, CHOLMOD) which pay a heavier symbolic tax in exchange for BLAS-3 speedups on larger fronts.
5+
CXSparse is the QR / LU / Cholesky counterpart to **KLU's design philosophy**: a small symbolic phase, very little per-call overhead, well-suited to small-to-medium sparse problems (n up to a few thousand). It complements the multifrontal solvers (UMFPACK, SPQR, CHOLMOD) which pay a heavier symbolic tax in exchange for BLAS-3 speedups on larger fronts.
66

7-
This package wraps the `cs_qr` and `cs_lu` factorizations from `CXSparse_jll` and exposes a Julian API.
7+
This package wraps the three direct factorizations CXSparse provides — `cs_qr`, `cs_lu`, and `cs_chol`and exposes a Julian `\` / `ldiv!` API on top.
88

99
## Status
1010

11-
- [x] `cs_qr` for `SparseMatrixCSC{Float64, Int32 | Int64}` and `SparseMatrixCSC{ComplexF64, Int32 | Int64}`
12-
- [x] `cs_lu` for the same four element/index combinations
13-
- [x] `LinearAlgebra.ldiv!` and `\` for both factorizations
14-
- [x] Finalizer-managed C-side memory (no manual `cs_*_sfree` calls required)
15-
- [ ] `cs_cholsol` for symmetric positive definite — not yet wrapped
11+
- [x] `cs_qr` — QR factorization (square or overdetermined `m ≥ n`)
12+
- [x] `cs_lu` — LU factorization (square)
13+
- [x] `cs_cholesky` — Cholesky factorization (square symmetric/Hermitian positive definite)
14+
- [x] All four CXSparse element/index combinations: `Float64` / `ComplexF64` × `Int32` / `Int64`
15+
- [x] `LinearAlgebra.ldiv!` and `\` on every factorization type
16+
- [x] Finalizer-managed C-side memory (no manual `cs_*_sfree` / `cs_*_nfree` calls)
1617

1718
## Quick start
1819

1920
```julia
20-
using CXSparse, SparseArrays
21+
using CXSparse, SparseArrays, LinearAlgebra
2122

2223
A = sparse([2.0 1.0 0.0;
2324
1.0 3.0 1.0;
2425
0.0 1.0 2.0])
2526
b = [1.0, 2.0, 3.0]
2627

27-
# QR factorization (works for square or overdetermined m ≥ n):
28+
# QR factorization (square or overdetermined m ≥ n):
2829
F = cs_qr(A)
2930
x = F \ b
30-
# or in-place:
31+
# or in-place into a preallocated buffer:
3132
x_pre = zeros(3)
3233
ldiv!(x_pre, F, b)
3334

34-
# LU factorization (square only):
35+
# LU factorization (square):
3536
G = cs_lu(A)
3637
y = G \ b
38+
39+
# Cholesky factorization (square symmetric/Hermitian positive definite —
40+
# only the upper triangle of A is read):
41+
H = cs_cholesky(A)
42+
z = H \ b
3743
```
3844

45+
The factorization objects (`CSQR`, `CSLU`, `CSCholesky`) own all C-side memory and free it via a finalizer; you can also call `finalize(F)` explicitly to release it eagerly.
46+
3947
## Supported element/index combinations
4048

4149
| element type | index type | CXSparse name |
@@ -45,22 +53,24 @@ y = G \ b
4553
| `ComplexF64` | `Int32` | `cs_ci_*` |
4654
| `ComplexF64` | `Int64` | `cs_cl_*` |
4755

48-
CXSparse does **not** ship `Float32` variants — `cs_si_*` / `cs_sl_*` don't exist upstream.
56+
CXSparse does **not** ship `Float32` variants — `cs_si_*` / `cs_sl_*` don't exist upstream. For complex inputs, `cs_cholesky` computes the Hermitian factorization (`L * L' = A`, where `A = A'`).
4957

50-
## When to use this vs SPQR / UMFPACK
58+
## When to use this vs SPQR / UMFPACK / CHOLMOD
5159

52-
CXSparse `cs_qr` and `cs_lu` are textbook implementations: simple, low symbolic-phase overhead, no BLAS-3. They sit alongside KLU in the "small/medium fast path" niche:
60+
CXSparse's `cs_qr`, `cs_lu`, and `cs_chol` are textbook implementations: simple, low symbolic-phase overhead, no BLAS-3. They sit alongside KLU in the "small/medium fast path" niche:
5361

54-
| | KLU | UMFPACK | SPQR | CXSparse `cs_qr` / `cs_lu` |
55-
|---|---|---|---|---|
56-
| factorization | LU | LU | QR | LU / QR |
57-
| algorithm | BTF + Gilbert–Peierls partial-pivot LU | multifrontal BLAS-3 LU | multifrontal BLAS-3 QR | textbook Householder QR / Gilbert–Peierls LU |
58-
| sweet spot | n ≲ few thousand | n ≳ 1k | n ≳ 1k, least-squares | n ≲ few thousand |
59-
| pivoting | partial | row + column | sparsity-only (rank-revealing if `tol ≥ 0`) | partial (LU), sparsity-only (QR) |
60-
| BLAS-3 | no | yes | yes | no |
62+
| | KLU | UMFPACK | SPQR | CHOLMOD | CXSparse `cs_qr` / `cs_lu` / `cs_cholesky` |
63+
|---|---|---|---|---|---|
64+
| factorization | LU | LU | QR | Cholesky | LU / QR / Cholesky |
65+
| algorithm | BTF + Gilbert–Peierls partial-pivot LU | multifrontal BLAS-3 LU | multifrontal BLAS-3 QR | supernodal BLAS-3 Cholesky | textbook Householder QR / Gilbert–Peierls LU / up-looking Cholesky |
66+
| sweet spot | n ≲ few thousand | n ≳ 1k | n ≳ 1k, least-squares | n ≳ 1k SPD | n ≲ few thousand |
67+
| pivoting | partial | row + column | sparsity-only (rank-revealing if `tol ≥ 0`) | symmetric (no numerical pivoting) | partial (LU), sparsity-only (QR), symmetric (Cholesky) |
68+
| BLAS-3 | no | yes | yes | yes | no |
6169

6270
On 199×199 sparse matrices we observed `cs_qr` running in ~325 µs vs SPQR's ~570 µs (1.7× faster). For rank-deficient inputs, however, `cs_qr` is **not rank-revealing**`x` may contain non-finite entries from the back-solve dividing through near-zero `R` diagonals. If you need a clean solution on rank-deficient systems, fall back to `qr(A)` (SPQR) or LAPACK's column-pivoted QR on `Matrix(A)`.
6371

72+
`cs_cholesky` returns an error if the matrix is not positive definite (CXSparse aborts the factorization as soon as it hits a non-positive pivot). For semi-definite or indefinite symmetric systems, use `LDLFactorizations.jl` or fall back to `cs_lu`.
73+
6474
## Internals
6575

6676
CXSparse uses 0-based indexing. The wrapper allocates 0-based `colptr` / `rowval` buffers when constructing the `cs_*_sparse` view; `nzval` is shared with the user's `SparseMatrixCSC` directly (CXSparse doesn't mutate it during factorization of a CSC matrix).

src/CXSparse.jl

Lines changed: 150 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,19 @@
1-
"""
2-
CXSparse
3-
4-
Julia wrapper around the CXSparse library from SuiteSparse — the lightweight,
5-
textbook-style sparse direct solver from Tim Davis's *Direct Methods for
6-
Sparse Linear Systems* (CSparse), extended to support `ComplexF64` values and
7-
both 32- and 64-bit indices.
8-
9-
CXSparse is the QR / LU / Cholesky counterpart to KLU's design philosophy: a
10-
small symbolic-phase cost and very little per-call overhead, well-suited to
11-
small-to-medium sparse problems (n up to a few thousand). It complements the
12-
multifrontal solvers (UMFPACK, SPQR, CHOLMOD) which pay a heavier symbolic
13-
tax in exchange for BLAS-3 speedups on larger fronts.
14-
15-
Supported element / index combinations:
16-
17-
| element type | index type | CXSparse name |
18-
|----------------|------------|---------------|
19-
| `Float64` | `Int32` | `cs_di_*` |
20-
| `Float64` | `Int64` | `cs_dl_*` |
21-
| `ComplexF64` | `Int32` | `cs_ci_*` |
22-
| `ComplexF64` | `Int64` | `cs_cl_*` |
23-
24-
Public API:
25-
- `cs_qr(A)` — symbolic + numeric QR factorization; returns a `CSQR`.
26-
- `F \\ b`, `ldiv!(x, F, b)` — least-squares solve using a `CSQR`.
27-
- `cs_lu(A)` — symbolic + numeric LU factorization; returns a `CSLU`.
28-
- `F \\ b`, `ldiv!(x, F, b)` — solve using a `CSLU`.
29-
30-
C-side memory is freed when the Julia factorization object is garbage
31-
collected; explicit `finalize(F)` is also supported.
32-
"""
331
module CXSparse
342

353
using CXSparse_jll: libcxsparse
364
using LinearAlgebra
375
using SparseArrays: SparseArrays, SparseMatrixCSC, getcolptr, rowvals, nonzeros
386

39-
export cs_qr, cs_lu, CSQR, CSLU
7+
export cs_qr, cs_lu, cs_cholesky, CSQR, CSLU, CSCholesky
408

419
# CXSparse uses 0-based column orderings; pick COLAMD-ish ordering for QR.
4210
# The CSparse `order` argument: 0 = natural, 1 = AMD(A+A'), 2 = AMD(S'S),
4311
# 3 = AMD(A'A). For QR we pass 3 (AMD on A'A).
4412
const CS_ORDER_QR = Int32(3)
4513
# For LU: 2 = AMD(S'S) — Davis's recommended choice for unsymmetric LU.
4614
const CS_ORDER_LU = Int32(2)
15+
# For Cholesky: 1 = AMD(A+A') — symmetric ordering.
16+
const CS_ORDER_CHOL = Int32(1)
4717
# LU pivoting tolerance: 1.0 = strict partial pivoting; smaller values relax it.
4818
const CS_LU_TOL = 1.0
4919

@@ -519,4 +489,151 @@ function Base.:\(F::CSLU{Tv,Ti,T_sp}, b::AbstractVector{Tv}) where {Tv,Ti,T_sp}
519489
return ldiv!(x, F, b)
520490
end
521491

492+
# ---------------------------------------------------------------------------
493+
# Cholesky (symmetric/Hermitian positive definite)
494+
# ---------------------------------------------------------------------------
495+
# CXSparse's `cs_*_chol` produces a lower-triangular L with L*L' = P*A*P',
496+
# where P is the AMD(A+A') fill-reducing permutation stored in S->pinv.
497+
# Only the upper triangle of A is read; the matrix is assumed symmetric
498+
# (for Float64) or Hermitian (for ComplexF64).
499+
"""
500+
CSCholesky{Tv,Ti}
501+
502+
Symbolic + numeric CXSparse Cholesky factorization of a square symmetric
503+
(real) or Hermitian (complex) positive-definite `SparseMatrixCSC{Tv,Ti}`.
504+
Holds opaque pointers to the C-side `cs_*s` symbolic and `cs_*n` numeric
505+
structs; freed by a finalizer or explicit `finalize`.
506+
"""
507+
mutable struct CSCholesky{Tv,Ti,T_sp}
508+
view::_CSView{T_sp,Tv,Ti}
509+
S::Ptr{Cvoid}
510+
N::Ptr{Cvoid}
511+
n::Int
512+
end
513+
514+
for (Tv, Ti, tag) in (
515+
(Float64, Int32, :di),
516+
(Float64, Int64, :dl),
517+
(ComplexF64, Int32, :ci),
518+
(ComplexF64, Int64, :cl),
519+
)
520+
sparse_ty = Symbol("cs_$(tag)")
521+
schol_sym = "cs_$(tag)_schol"
522+
chol_sym = "cs_$(tag)_chol"
523+
ltsolve_sym = "cs_$(tag)_ltsolve"
524+
pvec_sym = "cs_$(tag)_pvec"
525+
526+
@eval begin
527+
function _cs_schol(view::_CSView{$sparse_ty,$Tv,$Ti})
528+
return @ccall libcxsparse.$schol_sym(
529+
CS_ORDER_CHOL::$Ti,
530+
Ref(view.sparse)::Ref{$sparse_ty},
531+
)::Ptr{Cvoid}
532+
end
533+
function _cs_chol(view::_CSView{$sparse_ty,$Tv,$Ti}, S::Ptr{Cvoid})
534+
return @ccall libcxsparse.$chol_sym(
535+
Ref(view.sparse)::Ref{$sparse_ty},
536+
S::Ptr{Cvoid},
537+
)::Ptr{Cvoid}
538+
end
539+
function _cs_ltsolve!(::Type{$Ti}, L::Ptr{Cvoid}, b::AbstractVector{$Tv})
540+
@ccall libcxsparse.$ltsolve_sym(
541+
L::Ptr{Cvoid},
542+
pointer(b)::Ptr{$Tv},
543+
)::$Ti
544+
end
545+
function _cs_pvec!(
546+
p::Ptr{$Ti},
547+
b::AbstractVector{$Tv},
548+
x::AbstractVector{$Tv},
549+
n::Integer,
550+
)
551+
@ccall libcxsparse.$pvec_sym(
552+
p::Ptr{$Ti},
553+
pointer(b)::Ptr{$Tv},
554+
pointer(x)::Ptr{$Tv},
555+
$Ti(n)::$Ti,
556+
)::$Ti
557+
end
558+
end
559+
end
560+
561+
"""
562+
cs_cholesky(A::SparseMatrixCSC) -> CSCholesky
563+
564+
Compute the symbolic and numeric CXSparse Cholesky factorization of `A`.
565+
`A` must be square and symmetric (real) or Hermitian (complex) positive
566+
definite. Only the upper triangle of `A` is read.
567+
568+
Throws if the factorization fails (typically: `A` is not positive definite,
569+
or numerical breakdown encountered a non-positive pivot).
570+
571+
Solve with `F \\ b` or `ldiv!(x, F, b)`.
572+
"""
573+
function cs_cholesky end
574+
575+
for (Tv, Ti, tag) in (
576+
(Float64, Int32, :di),
577+
(Float64, Int64, :dl),
578+
(ComplexF64, Int32, :ci),
579+
(ComplexF64, Int64, :cl),
580+
)
581+
sparse_ty = Symbol("cs_$(tag)")
582+
@eval function cs_cholesky(A::SparseMatrixCSC{$Tv,$Ti})
583+
m, n = size(A)
584+
m == n || error(
585+
"CXSparse cs_cholesky requires a square matrix; got $(size(A))")
586+
view = _csview(A)
587+
S = _cs_schol(view)
588+
S == C_NULL && error("CXSparse cs_*_schol returned NULL")
589+
N = _cs_chol(view, S)
590+
if N == C_NULL
591+
_cs_sfree($Tv, $Ti, S)
592+
error("CXSparse cs_*_chol returned NULL — matrix not positive " *
593+
"definite (or not symmetric/Hermitian)?")
594+
end
595+
F = CSCholesky{$Tv,$Ti,$sparse_ty}(view, S, N, n)
596+
finalizer(_finalize_chol!, F)
597+
return F
598+
end
599+
end
600+
601+
function _finalize_chol!(F::CSCholesky{Tv,Ti,T_sp}) where {Tv,Ti,T_sp}
602+
_cs_sfree(Tv, Ti, F.S); _cs_nfree(Tv, Ti, F.N)
603+
F.S = C_NULL; F.N = C_NULL
604+
return nothing
605+
end
606+
607+
Base.size(F::CSCholesky) = (F.n, F.n)
608+
Base.size(F::CSCholesky, d::Integer) = d == 1 || d == 2 ? F.n : 1
609+
610+
function LinearAlgebra.ldiv!(
611+
x::AbstractVector{Tv},
612+
F::CSCholesky{Tv,Ti,T_sp},
613+
b::AbstractVector{Tv},
614+
) where {Tv,Ti,T_sp}
615+
n = F.n
616+
length(b) == n || throw(DimensionMismatch(
617+
"rhs has length $(length(b)), expected $n"))
618+
length(x) == n || throw(DimensionMismatch(
619+
"solution has length $(length(x)), expected $n"))
620+
work = Vector{Tv}(undef, n)
621+
Spinv = _cs_S_pinv(Tv, Ti, F.S)
622+
L = _cs_N_L(Tv, Ti, F.N)
623+
# work = P*b
624+
_cs_ipvec!(Spinv, b, work, n)
625+
# work = L \ work
626+
_cs_lsolve!(Ti, L, work)
627+
# work = L' \ work
628+
_cs_ltsolve!(Ti, L, work)
629+
# x = P' * work
630+
_cs_pvec!(Spinv, work, x, n)
631+
return x
632+
end
633+
634+
function Base.:\(F::CSCholesky{Tv,Ti,T_sp}, b::AbstractVector{Tv}) where {Tv,Ti,T_sp}
635+
x = Vector{Tv}(undef, F.n)
636+
return ldiv!(x, F, b)
637+
end
638+
522639
end # module

test/runtests.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,66 @@ end
107107
end
108108
end
109109

110+
@testset "CXSparse cs_cholesky" begin
111+
Random.seed!(0xCC55_F00D)
112+
113+
@testset "SPD ($Tv, $Ti, n=$n)" for Tv in ELTYPES,
114+
Ti in IDXTYPES, n in (5, 25, 100)
115+
116+
# SPD (real) / Hermitian PD (complex) via M*M' + n*I.
117+
Mraw = Tv <: Complex ? complex.(randn(n, n), randn(n, n)) : randn(n, n)
118+
Adense = Mraw * Mraw' + Tv(n) * I
119+
# Adense is dense Float64 / ComplexF64 either way; sparsify and convert.
120+
A = _convert(sparse(Adense), Tv, Ti)
121+
b_ = Tv <: Complex ? complex.(randn(n), randn(n)) : randn(n)
122+
b = Vector{Tv}(b_)
123+
124+
F = cs_cholesky(A)
125+
@test size(F) == (n, n)
126+
@test size(F, 1) == n
127+
@test size(F, 2) == n
128+
129+
x = F \ b
130+
@test length(x) == n
131+
@test norm(A * x - b) < 1e-8 * norm(b)
132+
133+
x_pre = zeros(Tv, n)
134+
@test ldiv!(x_pre, F, b) === x_pre
135+
@test x_pre x
136+
end
137+
138+
@testset "rejects non-square ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
139+
A = _convert(sparse(randn(3, 5)), Tv, Ti)
140+
@test_throws ErrorException cs_cholesky(A)
141+
end
142+
143+
@testset "rejects non-positive-definite" begin
144+
# Negative-definite — cs_chol returns NULL on the first non-positive
145+
# pivot, which we surface as an error.
146+
A = sparse(Float64[-1.0 0.0; 0.0 -1.0])
147+
@test_throws ErrorException cs_cholesky(A)
148+
end
149+
150+
@testset "dimension checks" begin
151+
A = sparse(Float64[2.0 0.0; 0.0 2.0])
152+
F = cs_cholesky(A)
153+
@test_throws DimensionMismatch (F \ [1.0, 2.0, 3.0])
154+
@test_throws DimensionMismatch ldiv!(zeros(3), F, [1.0, 2.0])
155+
end
156+
end
157+
110158
@testset "Finalizer doesn't crash" begin
111159
# Force GC of factorizations and ensure we don't double-free.
112160
for _ in 1:50
113161
A = sparse(randn(10, 10) + 10 * I)
162+
Asym = Matrix(A) * Matrix(A)' + 10 * I
163+
Asym = sparse(Asym)
114164
F = cs_qr(A)
115165
F2 = cs_lu(A)
166+
F3 = cs_cholesky(Asym)
116167
_ = F \ randn(10)
117168
_ = F2 \ randn(10)
169+
_ = F3 \ randn(10)
118170
end
119171
GC.gc()
120172
GC.gc()

0 commit comments

Comments
 (0)