Skip to content

Commit 4e1a70b

Browse files
authored
Merge pull request #225 from JuliaArrays/cjf-matrix-inverse
Another try at 4x4 matrix inverses
2 parents 80b6bac + 068a6d9 commit 4e1a70b

File tree

6 files changed

+150
-7
lines changed

6 files changed

+150
-7
lines changed

src/SUnitRange.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ end
1919
@pure SUnitRange(a::Int, b::Int) = SUnitRange{a, max(0, b - a + 1)}()
2020

2121
@pure @propagate_inbounds function getindex(x::SUnitRange{Start, L}, i::Int) where {Start, L}
22-
@boundscheck if i < Start || i >= (Start + L)
22+
@boundscheck if i < 1 || i > L
2323
throw(BoundsError(x, i))
2424
end
2525
return Start + i - 1

src/det.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ end
2424
return vecdot(x0, cross(x1, x2))
2525
end
2626

27-
@generated function _det{S,T}(::Size{S}, A::StaticMatrix{T})
27+
@generated function _det{S}(::Size{S}, A::StaticMatrix)
2828
if S[1] != S[2]
2929
throw(DimensionMismatch("matrix is not square"))
3030
end
3131
return quote # Implementation from Base
3232
@_inline_meta
33+
T = eltype(A)
3334
T2 = typeof((one(T)*zero(T) + zero(T))/one(T))
3435
if istriu(A) || istril(A)
3536
return convert(T2, det(UpperTriangular(A))) # Is this a Julia bug that a convert is not type stable??

src/inv.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ end
3030
@inbounds return newtype((y0[1], y1[1], y2[1], y0[2], y1[2], y2[2], y0[3], y1[3], y2[3]))
3131
end
3232

33+
Base.@pure function splitrange(r::SUnitRange)
34+
mid = (first(r) + last(r)) ÷ 2
35+
(SUnitRange(first(r), mid), SUnitRange(mid+1, last(r)))
36+
end
37+
38+
@noinline function _inv(::Size{(4,4)}, A)
39+
# Partition matrix into four 2x2 blocks. For 4x4 matrices this seems to be
40+
# more stable than directly using the adjugate expansion.
41+
# See http://www.freevec.org/function/inverse_matrix_4x4_using_partitioning
42+
#
43+
# TODO: This decomposition works in higher dimensions, but numerical
44+
# stability doesn't seem good for badly conditioned matrices. Can be
45+
# fixed?
46+
(i1,i2) = splitrange(SUnitRange(1,4))
47+
@inbounds P = A[i1,i1]
48+
@inbounds Q = A[i1,i2]
49+
@inbounds R = A[i2,i1]
50+
@inbounds S = A[i2,i2]
51+
invP = inv(P)
52+
invP_Q = invP*Q
53+
S2 = inv(S - R*invP_Q)
54+
R2 = -S2*(R*invP)
55+
Q2 = -invP_Q*S2
56+
P2 = invP - invP_Q*R2
57+
[[P2 Q2];
58+
[R2 S2]]
59+
end
60+
3361
@inline function _inv(::Size, A)
3462
T = eltype(A)
3563
S = typeof((one(T)*zero(T) + zero(T))/one(T))

test/SUnitRange.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
@test length(StaticArrays.SUnitRange(1,3)) === 3
33
@test length(StaticArrays.SUnitRange(1,-10)) === 0
44

5+
@test_throws BoundsError StaticArrays.SUnitRange(2,4)[0]
6+
@test StaticArrays.SUnitRange(2,4)[1] === 2
57
@test StaticArrays.SUnitRange(2,4)[2] === 3
6-
7-
@test_throws BoundsError StaticArrays.SUnitRange(1,3)[4]
8+
@test StaticArrays.SUnitRange(2,4)[3] === 4
9+
@test_throws BoundsError StaticArrays.SUnitRange(2,4)[4]
10+
811
@test_throws Exception StaticArrays.SUnitRange{1, -1}()
912
@test_throws TypeError StaticArrays.SUnitRange{1, 1.5}()
1013
end

test/det.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#triu/tril
88
@test det(@SMatrix [1 2; 0 3]) === 3
99
@test det(@SMatrix [1 2 3 4; 0 5 6 7; 0 0 8 9; 0 0 0 10]) === 400.0
10+
@test @inferred(det(ones(SMatrix{10,10,Complex{Float64}}))) == 0
1011

1112
# Unsigned specializations
1213
@test det(@SMatrix [0x00 0x01; 0x01 0x00])::Int8 == -1

test/inv.jl

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,125 @@
1+
using StaticArrays, Base.Test
2+
3+
"""
4+
Create an almost singular `Matrix{Float64}` of size `N×N`.
5+
6+
The first `rank` columns are chosen randomly, followed by `N-rank` linear
7+
combinations with random weights to create an `N×N` singular matrix `S`.
8+
A noise matrix of norm `ϵ*norm(S)` is added.
9+
10+
The condition number of this matrix should be approximately ϵ/eps(Float64)
11+
"""
12+
function almost_singular_matrix(N, rank, ϵ)
13+
B = rand(N,rank)
14+
weights = rand(rank, N-rank) ./ rank
15+
S = [B B*weights]
16+
S = S ./ norm(S)
17+
noise = rand(N,N)
18+
S + ϵ*norm(S)/norm(noise) * noise
19+
end
20+
121
@testset "Matrix inverse" begin
222
@test inv(@SMatrix [2])::SMatrix @SMatrix [0.5]
323
@test inv(@SMatrix [1 2; 2 1])::SMatrix [-1/3 2/3; 2/3 -1/3]
424
@test inv(@SMatrix [1 2 0; 2 1 0; 0 0 1])::SMatrix [-1/3 2/3 0; 2/3 -1/3 0; 0 0 1]
5-
m = randn(Float64, 4,4) + eye(4) # well conditioned
6-
@test inv(SMatrix{4,4}(m))::StaticMatrix inv(m)
725

26+
m = randn(Float64, 10,10) + eye(10) # well conditioned
27+
@test inv(SMatrix{10,10}(m))::StaticMatrix inv(m)
28+
29+
30+
# Unsigned versions
831
@test inv(@SMatrix [0x01 0x02; 0x02 0x01])::SMatrix [-1/3 2/3; 2/3 -1/3]
932
@test inv(@SMatrix [0x01 0x02 0x00; 0x02 0x01 0x00; 0x00 0x00 0x01])::SMatrix [-1/3 2/3 0; 2/3 -1/3 0; 0 0 1]
10-
33+
1134
m = triu(randn(Float64, 4,4) + eye(4))
1235
@test inv(SMatrix{4,4}(m))::StaticMatrix inv(m)
1336
m = tril(randn(Float64, 4,4) + eye(4))
1437
@test inv(SMatrix{4,4}(m))::StaticMatrix inv(m)
1538
end
39+
40+
41+
@testset "Matrix inverse 4x4" begin
42+
# A random well-conditioned matrix generated with `rand(1:10,4,4)`
43+
m = Float64[8 4 3 1;
44+
9 1 9 8;
45+
5 0 2 1;
46+
7 5 7 2]
47+
sm = SMatrix{4,4}(m)
48+
@test isapprox(inv(sm)::StaticMatrix, inv(m), rtol=2e-15)
49+
50+
# Poorly conditioned matrix; almost_singular_matrix(4, 3, 1e-7)
51+
m = [
52+
2.83056817904263402e-01 1.26822318692296848e-01 2.59665505365002547e-01 1.24524798964590747e-01
53+
4.29869029098094768e-01 2.29977256378012789e-03 4.63841570354149135e-01 1.51975221831027241e-01
54+
2.33883312850939995e-01 7.69469213907006122e-02 3.16525493661056589e-01 1.06181613512460249e-01
55+
1.63327582123091175e-01 1.70552365412100865e-01 4.55720450934200327e-01 1.23299968650232419e-01
56+
]
57+
sm = SMatrix{4,4}(m)
58+
@test norm(Matrix(sm*inv(sm) - eye(4))) < 10*norm(m*inv(m) - eye(4))
59+
@test norm(Matrix(inv(sm)*sm - eye(4))) < 10*norm(inv(m)*m - eye(4))
60+
61+
# Poorly conditioned matrix, generated by fuzz testing with
62+
# almost_singular_matrix(N, 3, 1e-7)
63+
# Case where the 4x4 algorithm fares badly compared to Base inv
64+
m = [1.80589503332920231e-02 2.37595944073211218e-01 3.80770755039433806e-01 5.70474286930408372e-02
65+
2.17494123374967520e-02 2.94069278741667661e-01 2.57334651147732463e-01 4.02348559939922357e-02
66+
3.42945636322714076e-01 3.29329508494837497e-01 2.25635541033857107e-01 6.03912636987153917e-02
67+
4.77828437366344727e-01 4.86406974015710591e-01 1.95415684569693188e-01 6.74775080892497797e-02]
68+
sm = SMatrix{4,4}(m)
69+
@test_broken norm(Matrix(inv(sm)*sm - eye(4))) < 10*norm(inv(m)*m - eye(4))
70+
@test_broken norm(Matrix(sm*inv(sm) - eye(4))) < 10*norm(m*inv(m) - eye(4))
71+
end
72+
73+
74+
#-------------------------------------------------------------------------------
75+
# More comprehensive but qualitiative testing for inv() accuracy
76+
#=
77+
using PyPlot
78+
79+
inv_residual(A::AbstractMatrix) = norm(A*inv(A) - eye(size(A,1)))
80+
81+
"""
82+
plot_residuals(N, rank, ϵ)
83+
84+
Plot `inv_residual(::StaticMatrix)` vs `inv_residual(::Matrix)`
85+
86+
"""
87+
function plot_residuals(N, rank, ϵ)
88+
A_residuals = []
89+
SA_residuals = []
90+
for i=1:10000
91+
A = almost_singular_matrix(N, rank, ϵ)
92+
SA = SMatrix{N,N}(A)
93+
SA_residual = norm(Matrix(SA*inv(SA) - eye(N)))
94+
A_residual = norm(A*inv(A) - eye(N))
95+
push!(SA_residuals, SA_residual)
96+
push!(A_residuals, A_residual)
97+
#= if SA_residual/A_residual > 10000 =#
98+
#= @printf("%10e %.4f\n", SA_residual, SA_residual/A_residual) =#
99+
#= println("[") =#
100+
#= for i=1:4 =#
101+
#= for j=1:4 =#
102+
#= @printf("%.17e ", A[i,j]) =#
103+
#= end =#
104+
#= println() =#
105+
#= end =#
106+
#= println("]") =#
107+
#= end =#
108+
end
109+
loglog(A_residuals, SA_residuals, ".", markersize=1.5)
110+
end
111+
112+
# Plot the accuracy of inv implementations for almost singular matrices of
113+
# various rank
114+
clf()
115+
N = 4
116+
title("inv() accuracy for poorly conditioned $(N)x$(N) - Base vs block decomposition")
117+
labels = []
118+
for i in N:-1:1
119+
plot_residuals(N, i, 1e-7)
120+
push!(labels, "rank $i")
121+
end
122+
xlabel("Residual norm: `inv(::Array)`")
123+
ylabel("Residual norm: `inv(::StaticArray)`")
124+
legend(labels)
125+
=#

0 commit comments

Comments
 (0)