Skip to content

Commit 3028659

Browse files
Merge pull request #2 from JuliaSparse/sciml-test-setup-and-correctness
Add SciML test setup and correctness tests
2 parents b8f2ee8 + b0e9fc6 commit 3028659

10 files changed

Lines changed: 689 additions & 166 deletions

File tree

.github/workflows/CI.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CI
2+
on:
3+
push:
4+
branches:
5+
- main
6+
tags: ['*']
7+
pull_request:
8+
workflow_dispatch:
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.run_id }}
11+
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
12+
jobs:
13+
test:
14+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.group }}
15+
runs-on: ${{ matrix.os }}
16+
timeout-minutes: 60
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
version:
21+
- '1.11'
22+
- '1'
23+
os:
24+
- ubuntu-latest
25+
group:
26+
- Core
27+
- QA
28+
include:
29+
- version: '1'
30+
os: macos-latest
31+
group: Core
32+
- version: '1'
33+
os: windows-latest
34+
group: Core
35+
steps:
36+
- uses: actions/checkout@v4
37+
- uses: julia-actions/setup-julia@v2
38+
with:
39+
version: ${{ matrix.version }}
40+
arch: x64
41+
- uses: julia-actions/cache@v2
42+
- uses: julia-actions/julia-buildpkg@v1
43+
- uses: julia-actions/julia-runtest@v1
44+
env:
45+
GROUP: ${{ matrix.group }}
46+
- uses: julia-actions/julia-processcoverage@v1
47+
- uses: codecov/codecov-action@v4
48+
with:
49+
files: lcov.info
50+
token: ${{ secrets.CODECOV_TOKEN }}
51+
fail_ci_if_error: false

Project.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
99
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1010

1111
[compat]
12-
CXSparse_jll = "4"
12+
Aqua = "0.8"
13+
CXSparse_jll = "4, 400"
14+
ExplicitImports = "1"
1315
LinearAlgebra = "1"
16+
Random = "1"
17+
SafeTestsets = "0.1"
1418
SparseArrays = "1"
15-
julia = "1.10"
19+
Test = "1"
20+
julia = "1.11"
1621

1722
[extras]
23+
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
24+
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
1825
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
26+
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
1927
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2028

2129
[targets]
22-
test = ["Random", "Test"]
30+
test = ["Aqua", "ExplicitImports", "Random", "SafeTestsets", "Test"]

src/CXSparse.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module CXSparse
22

33
using CXSparse_jll: libcxsparse
4-
using LinearAlgebra
4+
using LinearAlgebra: LinearAlgebra, ldiv!
55
using SparseArrays: SparseArrays, SparseMatrixCSC, getcolptr, rowvals, nonzeros
66

77
export cs_qr, cs_lu, cs_cholesky, CSQR, CSLU, CSCholesky

test/cholesky_tests.jl

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
include("shared.jl")
2+
3+
@testset "CXSparse cs_cholesky" begin
4+
Random.seed!(0xCC55_F00D)
5+
6+
@testset "SPD ($Tv, $Ti, n=$n)" for Tv in ELTYPES,
7+
Ti in IDXTYPES, n in (5, 25, 100)
8+
9+
Mraw = _randmat(Tv, n, n)
10+
Adense = Mraw * Mraw' + Tv(n) * I
11+
A = _convert(Adense, Tv, Ti)
12+
b = _randvec(Tv, n)
13+
14+
F = cs_cholesky(A)
15+
@test size(F) == (n, n)
16+
@test size(F, 1) == n
17+
@test size(F, 2) == n
18+
19+
x = F \ b
20+
@test length(x) == n
21+
@test norm(A * x - b) < 1e-8 * norm(b)
22+
@test x Adense \ b rtol=1e-8
23+
24+
x_pre = zeros(Tv, n)
25+
@test ldiv!(x_pre, F, b) === x_pre
26+
@test x_pre x
27+
end
28+
29+
@testset "rejects non-square ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
30+
A = _convert(_randmat(Tv, 3, 5), Tv, Ti)
31+
@test_throws ErrorException cs_cholesky(A)
32+
end
33+
34+
@testset "rejects non-positive-definite ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
35+
A = _convert(Tv[-1 0; 0 -1], Tv, Ti)
36+
@test_throws ErrorException cs_cholesky(A)
37+
end
38+
39+
@testset "rejects zero diagonal ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
40+
A = _convert(Tv[0 0; 0 1], Tv, Ti)
41+
@test_throws ErrorException cs_cholesky(A)
42+
end
43+
44+
@testset "dimension checks ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
45+
A = _convert(Tv[2 0; 0 2], Tv, Ti)
46+
F = cs_cholesky(A)
47+
@test_throws DimensionMismatch (F \ Tv[1, 2, 3])
48+
@test_throws DimensionMismatch ldiv!(zeros(Tv, 3), F, Tv[1, 2])
49+
end
50+
51+
@testset "n=1 trivial case ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
52+
A = _convert(reshape(Tv[9.0], 1, 1), Tv, Ti)
53+
F = cs_cholesky(A)
54+
@test F \ Tv[18.0] Tv[2.0]
55+
end
56+
57+
@testset "identity matrix ($Tv, $Ti, n=$n)" for Tv in ELTYPES,
58+
Ti in IDXTYPES, n in (3, 10)
59+
A = _convert(Matrix{Tv}(I, n, n), Tv, Ti)
60+
b = _randvec(Tv, n)
61+
F = cs_cholesky(A)
62+
@test F \ b b
63+
end
64+
65+
@testset "diagonal positive matrix ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
66+
d = Tv <: Complex ? Tv[2+0im, 3+0im, 4+0im, 1+0im] : Tv[2, 3, 4, 1]
67+
A = _convert(Diagonal(d), Tv, Ti)
68+
b = _randvec(Tv, 4)
69+
F = cs_cholesky(A)
70+
@test F \ b b ./ d
71+
end
72+
73+
@testset "tridiagonal SPD ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
74+
n = 25
75+
Adense = Tridiagonal(fill(Tv(-1), n - 1), fill(Tv(4), n), fill(Tv(-1), n - 1))
76+
A = _convert(Matrix(Adense), Tv, Ti)
77+
b = _randvec(Tv, n)
78+
F = cs_cholesky(A)
79+
@test F \ b Matrix(Adense) \ b rtol=1e-10
80+
end
81+
82+
@testset "Hermitian (not just real-symmetric) ($Ti)" for Ti in IDXTYPES
83+
# Hermitian PD matrix with non-trivial complex off-diagonals.
84+
Adense = ComplexF64[
85+
4.0+0.0im 1.0+0.5im 0.0+0.0im
86+
1.0-0.5im 3.0+0.0im 0.5-0.25im
87+
0.0+0.0im 0.5+0.25im 2.0+0.0im
88+
]
89+
@assert ishermitian(Adense)
90+
A = _convert(Adense, ComplexF64, Ti)
91+
b = ComplexF64[1+0im, 2-1im, -1+0.5im]
92+
F = cs_cholesky(A)
93+
x = F \ b
94+
@test x Adense \ b rtol=1e-10
95+
@test norm(A * x - b) < 1e-10 * norm(b)
96+
end
97+
98+
@testset "small known-answer ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
99+
# A = [4 2; 2 5] (SPD: dets are 4, 16) ; b = [6, 7]
100+
# det(A) = 16, x = [(5*6 - 2*7)/16, (4*7 - 2*6)/16] = [16/16, 16/16] = [1, 1]
101+
A = _convert(Tv[4 2; 2 5], Tv, Ti)
102+
b = Tv[6, 7]
103+
F = cs_cholesky(A)
104+
@test F \ b Tv[1, 1]
105+
end
106+
107+
@testset "input matrix is not mutated by factorization or solve ($Tv, $Ti)" for
108+
Tv in ELTYPES, Ti in IDXTYPES
109+
M = _randmat(Tv, 8, 8)
110+
A = _convert(M * M' + Tv(8) * I, Tv, Ti)
111+
snap = _snapshot(A)
112+
F = cs_cholesky(A)
113+
@test _unchanged(A, snap)
114+
_ = F \ _randvec(Tv, 8)
115+
@test _unchanged(A, snap)
116+
end
117+
118+
@testset "rhs `b` is not mutated by solve ($Tv, $Ti)" for Tv in ELTYPES, Ti in IDXTYPES
119+
M = _randmat(Tv, 6, 6)
120+
A = _convert(M * M' + Tv(6) * I, Tv, Ti)
121+
b = _randvec(Tv, 6)
122+
b_orig = copy(b)
123+
F = cs_cholesky(A)
124+
_ = F \ b
125+
@test b == b_orig
126+
_ = ldiv!(zeros(Tv, 6), F, b)
127+
@test b == b_orig
128+
end
129+
130+
@testset "multiple solves on same factorization ($Tv, $Ti)" for
131+
Tv in ELTYPES, Ti in IDXTYPES
132+
M = _randmat(Tv, 10, 10)
133+
A = _convert(M * M' + Tv(10) * I, Tv, Ti)
134+
F = cs_cholesky(A)
135+
Adense = Matrix(A)
136+
for _ in 1:5
137+
b = _randvec(Tv, 10)
138+
x = F \ b
139+
@test x Adense \ b rtol=1e-10
140+
end
141+
end
142+
143+
@testset "ldiv! returns the destination vector ($Tv, $Ti)" for
144+
Tv in ELTYPES, Ti in IDXTYPES
145+
M = _randmat(Tv, 5, 5)
146+
A = _convert(M * M' + Tv(5) * I, Tv, Ti)
147+
F = cs_cholesky(A)
148+
b = _randvec(Tv, 5)
149+
x = Vector{Tv}(undef, 5)
150+
@test ldiv!(x, F, b) === x
151+
end
152+
153+
@testset "explicit finalize is safe and idempotent" begin
154+
M = randn(5, 5)
155+
A = sparse(M * M' + 5 * I)
156+
F = cs_cholesky(A)
157+
_ = F \ randn(5)
158+
finalize(F)
159+
finalize(F)
160+
@test F.S == C_NULL
161+
@test F.N == C_NULL
162+
end
163+
end

test/cross_tests.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
include("shared.jl")
2+
3+
@testset "Cross-factorization consistency" begin
4+
Random.seed!(0xCC55_F00D)
5+
6+
# On a well-conditioned square non-singular matrix, cs_qr and cs_lu must
7+
# produce solutions that match each other (and the dense reference) to
8+
# high precision. On an SPD matrix, cs_cholesky must also agree.
9+
10+
@testset "QR vs LU on square non-singular ($Tv, $Ti, n=$n)" for
11+
Tv in ELTYPES, Ti in IDXTYPES, n in (8, 30)
12+
Adense = _randmat(Tv, n, n) + Tv(n) * I
13+
A = _convert(Adense, Tv, Ti)
14+
b = _randvec(Tv, n)
15+
16+
x_qr = cs_qr(A) \ b
17+
x_lu = cs_lu(A) \ b
18+
x_ref = Adense \ b
19+
@test x_qr x_lu rtol=1e-9
20+
@test x_qr x_ref rtol=1e-9
21+
@test x_lu x_ref rtol=1e-9
22+
end
23+
24+
@testset "QR vs LU vs Cholesky on SPD ($Tv, $Ti, n=$n)" for
25+
Tv in ELTYPES, Ti in IDXTYPES, n in (8, 30)
26+
M = _randmat(Tv, n, n)
27+
Adense = M * M' + Tv(n) * I
28+
A = _convert(Adense, Tv, Ti)
29+
b = _randvec(Tv, n)
30+
31+
x_qr = cs_qr(A) \ b
32+
x_lu = cs_lu(A) \ b
33+
x_chol = cs_cholesky(A) \ b
34+
x_ref = Adense \ b
35+
@test x_qr x_lu rtol=1e-9
36+
@test x_lu x_chol rtol=1e-9
37+
@test x_chol x_ref rtol=1e-9
38+
end
39+
end
40+
41+
@testset "Finalizer GC stress" begin
42+
# Repeatedly construct and discard factorizations; force GC to run finalizers
43+
# and ensure we don't double-free or crash.
44+
for _ in 1:50
45+
A = sparse(randn(10, 10) + 10 * I)
46+
M = randn(10, 10)
47+
Asym = sparse(M * M' + 10 * I)
48+
F = cs_qr(A)
49+
F2 = cs_lu(A)
50+
F3 = cs_cholesky(Asym)
51+
_ = F \ randn(10)
52+
_ = F2 \ randn(10)
53+
_ = F3 \ randn(10)
54+
end
55+
GC.gc()
56+
GC.gc()
57+
@test true
58+
end
59+
60+
@testset "Mixed element/index types interoperate" begin
61+
# Each (Tv, Ti) variant should work standalone in sequence (the @ccall
62+
# dispatch picks the correct cs_* symbol from libcxsparse based on the
63+
# generic-function method we defined).
64+
for Tv in (Float64, ComplexF64), Ti in (Int32, Int64)
65+
Adense = Tv <: Complex ?
66+
complex.(randn(6, 6), randn(6, 6)) + Tv(6) * I :
67+
randn(6, 6) + Tv(6) * I
68+
A = SparseMatrixCSC{Tv,Ti}(sparse(Adense))
69+
b = Tv <: Complex ?
70+
Vector{Tv}(complex.(randn(6), randn(6))) :
71+
Vector{Tv}(randn(6))
72+
F = cs_qr(A)
73+
x = F \ b
74+
@test norm(A * x - b) < 1e-8 * norm(b)
75+
end
76+
end

0 commit comments

Comments
 (0)