Skip to content

Commit 1faa137

Browse files
authored
Support pinv in Woodbury and SymWoodbury (#50)
When building a `Woodbury` or `SymWoodbury` matrix, we precompute (C⁻¹ + V * (A\U))⁻¹ However, if the final `W` is rank-deficient, this inverse does not exist. To handle this case, we now support an optional keyword argument `use_pinv::Bool=false` to use the pseudo-inverse instead. Also updates cache actions and adds dependabot support.
1 parent 7532dd5 commit 1faa137

File tree

9 files changed

+115
-55
lines changed

9 files changed

+115
-55
lines changed

.github/dependabot.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2+
version: 2
3+
updates:
4+
- package-ecosystem: "github-actions"
5+
directory: "/" # Location of package manifests
6+
schedule:
7+
interval: "monthly"

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
version:
18-
- '1.6'
18+
- 'min'
1919
- '1'
2020
os:
2121
- ubuntu-latest
2222
arch:
2323
- x64
2424
steps:
2525
- uses: actions/checkout@v2
26-
- uses: julia-actions/setup-julia@v1
26+
- uses: julia-actions/setup-julia@v2
2727
with:
2828
version: ${{ matrix.version }}
2929
arch: ${{ matrix.arch }}
30-
- uses: actions/cache@v1
30+
- uses: actions/cache@v4
3131
env:
3232
cache-name: cache-artifacts
3333
with:

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "WoodburyMatrices"
22
uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6"
3-
version = "1.0.0"
3+
version = "1.1.0"
44

55
[deps]
66
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -12,7 +12,7 @@ Random = "1"
1212
LinearAlgebra = "1"
1313
SparseArrays = "1"
1414
Test = "1"
15-
julia = "1.6"
15+
julia = "1.10"
1616

1717
[extras]
1818
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ If passed the keyword argument `allocatetmp=true`, (Sym)Woodbury allocates inter
4141
This eliminates memory allocation for these common operations.
4242

4343
However, using the same `W` across multiple threads can lead to race conditions. Hence, this optimization is opt-in and should only be used if you know it is safe.
44+
45+
## Rank-deficiency
46+
47+
While `A` is required to be invertible, rank-deficient `W` are supported if you supply the `use_pinv=true` keyword argument to the constructor,
48+
so that we compute `pinv(inv(C) + V*(A\U))` instead of the standard inverse.
49+
Then `x = W \ b` will effectively use the pseudo-inverse of `W`.

src/WoodburyMatrices.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ abstract type AbstractWoodbury{T} <: Factorization{T} end
1212
safeinv(A) = inv(A)
1313
safeinv(A::SparseMatrixCSC) = safeinv(Matrix(A))
1414

15+
safepinv(A) = pinv(A)
16+
safepinv(A::SparseMatrixCSC) = safepinv(Matrix(A))
17+
1518
include("woodbury.jl")
1619
include("symwoodbury.jl")
1720
include("sparsefactors.jl")

src/symwoodbury.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct SymWoodbury{T,AType,BType,DType,DpType} <: AbstractWoodbury{T}
1313
end
1414

1515
"""
16-
W = SymWoodbury(A, B, D; allocatetmp::Bool=false)
16+
W = SymWoodbury(A, B, D; allocatetmp::Bool=false, use_pinv::Bool=false)
1717
1818
Represent a matrix of the form `W = A + BDBᵀ`, where `A` and `D` are symmetric.
1919
Equations `Wx = b` will be solved using the
@@ -26,9 +26,9 @@ semidefinite matrix).
2626
This checks that `A` is symmetric; you can elide the check by passing a `Symmetric` matrix
2727
or factorization.
2828
29-
See also [Woodbury](@ref), where `allocatetmp` is explained.
29+
See also [Woodbury](@ref), where `allocatetmp` and `use_pinv` are explained.
3030
"""
31-
function SymWoodbury(A, B::AbstractVecOrMat, D; allocatetmp::Bool=false)
31+
function SymWoodbury(A, B::AbstractVecOrMat, D; allocatetmp::Bool=false, use_pinv::Bool=false)
3232
@noinline throwdmm(B, D, A) = throw(DimensionMismatch("Sizes of B ($(size(B))) and/or D ($(size(D))) are inconsistent with A ($(size(A)))"))
3333

3434
n = size(A, 1)
@@ -40,7 +40,8 @@ function SymWoodbury(A, B::AbstractVecOrMat, D; allocatetmp::Bool=false)
4040
issymmetric(A) || throw(ArgumentError("A must be symmetric"))
4141
end
4242
issymmetric(D) || throw(ArgumentError("D must be symmetric"))
43-
Dp = safeinv(safeinv(D) .+ B'*(A\B))
43+
Dpinv = safeinv(D) .+ B'*(A\B)
44+
Dp = use_pinv ? safepinv(Dpinv) : safeinv(Dpinv)
4445
# temporary space for allocation-free solver (vector RHS only)
4546
T = typeof(float(zero(eltype(A)) * zero(eltype(B)) * zero(eltype(D))))
4647
if allocatetmp

src/woodbury.jl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct Woodbury{T,AType,UType,VType,CType,CpType} <: AbstractWoodbury{T}
1414
end
1515

1616
"""
17-
W = Woodbury(A, U, C, V; allocatetmp::Bool=false)
17+
W = Woodbury(A, U, C, V; allocatetmp::Bool=false, use_pinv::Bool=false)
1818
1919
Represent a matrix `W = A + UCV`.
2020
Equations `Wx = b` will be solved using the
@@ -23,18 +23,20 @@ Equations `Wx = b` will be solved using the
2323
If your main goal is to solve equations, it's often advantageous to supply
2424
`A` as a factorization (e.g., `Woodbury(lu(A), U, C, V)`).
2525
26+
If `W` is rank-deficient or nearly so, setting `use_pinv` to true will use
27+
the pseudoinverse in the Woodbury formula to improve numerical stability.
28+
2629
If `allocatetmp` is true, temporary storage used for intermediate steps in
2730
multiplication and division will be allocated.
2831
2932
!!! warning
3033
If you'll use the same `W` in multiple threads, you should use `allocatetmp=false`
3134
or risk data races.
3235
33-
3436
See also [SymWoodbury](@ref).
3537
3638
"""
37-
function Woodbury(A, U::AbstractMatrix, C, V::AbstractMatrix; allocatetmp::Bool=false)
39+
function Woodbury(A, U::AbstractMatrix, C, V::AbstractMatrix; allocatetmp::Bool=false, use_pinv::Bool=false)
3840
@noinline throwdmm1(U, V, A) = throw(DimensionMismatch("Sizes of U ($(size(U))) and/or V ($(size(V))) are inconsistent with A ($(size(A)))"))
3941
@noinline throwdmm2(k) = throw(DimensionMismatch("C should be $(k)x$(k)"))
4042

@@ -48,7 +50,8 @@ function Woodbury(A, U::AbstractMatrix, C, V::AbstractMatrix; allocatetmp::Bool=
4850
throwdmm2(k)
4951
end
5052
end
51-
Cp = safeinv(safeinv(C) .+ V*(A\U))
53+
Cpinv = safeinv(C) .+ V*(A\U)
54+
Cp = use_pinv ? safepinv(Cpinv) : safeinv(Cpinv)
5255
# temporary space for allocation-free solver (vector RHS only)
5356
T = typeof(float(zero(eltype(A)) * zero(eltype(U)) * zero(eltype(C)) * zero(eltype(V))))
5457
if allocatetmp

test/symwoodbury.jl

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ for elty in (Float32, Float64, ComplexF32, ComplexF64, Int)
2626
ε = eps(abs2(float(one(elty))))
2727
A = Diagonal(a)
2828

29-
for W in (SymWoodbury(A, B, D), SymWoodbury(A, B, D; allocatetmp=true), SymWoodbury(A, B[:,1][:], 2.))
29+
for W in (SymWoodbury(A, B, D), SymWoodbury(A, B, D; allocatetmp=true), SymWoodbury(A, B[:,1][:], 2.), SymWoodbury(A, B, D; use_pinv=true))
3030

3131
@test issymmetric(W)
3232
F = Matrix(W)
@@ -131,48 +131,48 @@ end
131131
A = Diagonal((rand(n)))
132132
B = sprandn(n,2,1.)
133133
D = sprandn(2,2,1.); D = (D + D')/2
134-
W = SymWoodbury(A, B, D)
135-
v = randn(n)
136-
vdiag = Diagonal(v)
137-
V = randn(n,1)
138-
139-
@test size(W) == (n,n)
140-
@test size(W,1) == n
141-
@test size(W,2) == n
142-
143-
@test inv(W)*v ≈ inv(Matrix(W))*v
144-
@test (2*W)*v ≈ 2*(W*v)
145-
@test (W'W)*v Matrix(W)*(Matrix(W)*v)
146-
@test (W*W)*v Matrix(W)*(Matrix(W)*v)
147-
@test (W*W')*v ≈ Matrix(W)*(Matrix(W)*v)
148-
149-
@test inv(W)*vdiag ≈ W\vdiag
150-
@test W\vdiag ≈ W\Matrix(vdiag)
151-
@test inv(W)*vdiag ≈ inv(Matrix(W))*vdiag
152-
153-
@test inv(W)*V ≈ inv(Matrix(W))*V
154-
@test (2*W)*V ≈ 2*(W*V)
155-
@test (W'W)*V Matrix(W)*(Matrix(W)*V)
156-
@test (W*W)*V Matrix(W)*(Matrix(W)*V)
157-
@test (W*W')*V ≈ Matrix(W)*(Matrix(W)*V)
158-
159-
@test Matrix(2*W) ≈ 2*Matrix(W)
160-
@test Matrix(W*2) ≈ 2*Matrix(W)
161-
R = Symmetric(rand(size(W)...))
162-
# Not sure when this got fixed, but check that failures are for a "known" reason
163-
canadd = try (W + R; true;) catch err; (@test err isa MethodError && err.f == ldiv!; false;) end
164-
if canadd
165-
@test Matrix(W + R) ≈ Matrix(W) + R
166-
@test Matrix(R + W) ≈ Matrix(W) + R
167-
else
168-
@test_broken Matrix(W + R) ≈ Matrix(W) + R
169-
@test_broken Matrix(R + W) ≈ Matrix(W) + R
170-
end
171-
Wm = SymWoodbury(A, Matrix(B), D)
172-
@test Matrix(Wm + R) ≈ Matrix(Wm) + R
173-
@test Matrix(R + Wm) ≈ Matrix(Wm) + R
134+
for W in (SymWoodbury(A, B, D), SymWoodbury(A, B, D; use_pinv=true))
135+
v = randn(n)
136+
vdiag = Diagonal(v)
137+
V = randn(n,1)
138+
@test size(W) == (n,n)
139+
@test size(W,1) == n
140+
@test size(W,2) == n
141+
142+
@test inv(W)*v ≈ inv(Matrix(W))*v
143+
@test (2*W)*v ≈ 2*(W*v)
144+
@test (W'W)*v Matrix(W)*(Matrix(W)*v)
145+
@test (W*W)*v Matrix(W)*(Matrix(W)*v)
146+
@test (W*W')*v ≈ Matrix(W)*(Matrix(W)*v)
147+
148+
@test inv(W)*vdiag ≈ W\vdiag
149+
@test W\vdiag ≈ W\Matrix(vdiag)
150+
@test inv(W)*vdiag ≈ inv(Matrix(W))*vdiag
151+
152+
@test inv(W)*V ≈ inv(Matrix(W))*V
153+
@test (2*W)*V ≈ 2*(W*V)
154+
@test (W'W)*V Matrix(W)*(Matrix(W)*V)
155+
@test (W*W)*V Matrix(W)*(Matrix(W)*V)
156+
@test (W*W')*V ≈ Matrix(W)*(Matrix(W)*V)
157+
158+
@test Matrix(2*W) ≈ 2*Matrix(W)
159+
@test Matrix(W*2) ≈ 2*Matrix(W)
160+
R = Symmetric(rand(size(W)...))
161+
# Not sure when this got fixed, but check that failures are for a "known" reason
162+
canadd = try (W + R; true;) catch err; (@test err isa MethodError && err.f == ldiv!; false;) end
163+
if canadd
164+
@test Matrix(W + R) ≈ Matrix(W) + R
165+
@test Matrix(R + W) ≈ Matrix(W) + R
166+
else
167+
@test_broken Matrix(W + R) ≈ Matrix(W) + R
168+
@test_broken Matrix(R + W) ≈ Matrix(W) + R
169+
end
170+
Wm = SymWoodbury(A, Matrix(B), D)
171+
@test Matrix(Wm + R) ≈ Matrix(Wm) + R
172+
@test Matrix(R + Wm) ≈ Matrix(Wm) + R
174173
175-
@test transpose(W)*v ≈ transpose(Matrix(W))*v
174+
@test transpose(W)*v ≈ transpose(Matrix(W))*v
175+
end
176176
177177
# Factorization for A
178178
A = SymTridiagonal(rand(5).+2, rand(4))
@@ -205,6 +205,7 @@ W1 = SymWoodbury(A, B, D)
205205
@test_throws ArgumentError SymWoodbury(rand(5,5),rand(5,2),rand(2,2))
206206
207207
# Display
208+
W = SymWoodbury(A, B, D)
208209
iob = IOBuffer()
209210
show(iob, MIME("text/plain"), W)
210211
str = String(take!(iob))
@@ -243,4 +244,23 @@ end
243244
@test all(logabsdet(W) .≈ logabsdet(Matrix(W)))
244245
end
245246
247+
@testset "pinv" begin
248+
# Build a matrix W that is equivalent to
249+
# A = [Diagonal(c) ones(length(c))]
250+
# A'A
251+
# This is rank-deficient, so inv should fail but pinv should work
252+
c = [-1.0, -2.0, -3.0, -4.0]
253+
d = c.^2
254+
push!(d, length(c))
255+
U = [c zeros(length(c)); 0 1]
256+
@test_throws SingularException SymWoodbury(Diagonal(d), U, [0 1; 1 0])
257+
W = SymWoodbury(Diagonal(d), U, [0 1; 1 0]; use_pinv=true)
258+
b = randn(length(c)+1)
259+
# Project out the component along the singular direction
260+
E = eigen(Matrix(W); sortby=+)
261+
bproj = b - (E.vectors[:,begin]'*b)*E.vectors[:,begin]
262+
x = W \ bproj
263+
@test norm(W*x - bproj) 1e-10
264+
end
265+
246266
end # @testset "SymWoodbury"

test/woodbury.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,24 @@ W = Woodbury([randpsd(50) for _ in 1:4]...)
202202
@test all(logabsdet(W) .≈ logabsdet(Matrix(W)))
203203
end
204204
205+
@testset "pinv" begin
206+
# Build a matrix W that is equivalent to
207+
# A = [Diagonal(c) ones(length(c))]
208+
# A'A
209+
# This is rank-deficient, so inv should fail but pinv should work
210+
c = [-1.0, -2.0, -3.0, -4.0]
211+
d = c.^2
212+
push!(d, length(c))
213+
U = [c zeros(length(c)); 0 1]
214+
@test_throws SingularException Woodbury(Diagonal(d), U, [0 1; 1 0], U')
215+
W = Woodbury(Diagonal(d), U, [0 1; 1 0], U'; use_pinv=true)
216+
b = randn(length(c)+1)
217+
# Project out the component along the singular direction
218+
E = eigen(Matrix(W); sortby=+)
219+
bproj = b - (E.vectors[:,begin]'*b)*E.vectors[:,begin]
220+
x = W \ bproj
221+
@test norm(W*x - bproj) 1e-10
222+
end
223+
224+
205225
end # @testset "Woodbury"

0 commit comments

Comments
 (0)