Skip to content

Commit abe95f4

Browse files
committed
Specialized 4x4 matrix inversion based on partitioning
This version is fast and works well for well conditioned matrices. It seems somewhat better numerically conditioned than the adjugate expansion for poorly conditioned matrices, but can still fail quite badly in some cases.
1 parent e4a2562 commit abe95f4

File tree

2 files changed

+108
-3
lines changed

2 files changed

+108
-3
lines changed

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/inv.jl

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,92 @@
1+
"""
2+
Create an almost singular `Matrix{Float64}` of size `N×N`.
3+
4+
The first `rank` columns are chosen randomly, followed by `N-rank` linear
5+
combinations with random weights to create an `N×N` singular matrix `S`.
6+
A noise matrix of norm `ϵ*norm(S)` is added.
7+
8+
The condition number of this matrix should be approximately ϵ/eps(Float64)
9+
"""
10+
function almost_singular_matrix(N, rank, ϵ)
11+
B = rand(N,rank)
12+
weights = rand(rank, N-rank) ./ rank
13+
S = [B B*weights]
14+
S = S ./ norm(S)
15+
noise = rand(N,N)
16+
S + ϵ*norm(S)/norm(noise) * noise
17+
end
18+
119
@testset "Matrix inverse" begin
220
@test inv(@SMatrix [2])::SMatrix @SMatrix [0.5]
321
@test inv(@SMatrix [1 2; 2 1])::SMatrix [-1/3 2/3; 2/3 -1/3]
422
@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)
723

24+
m = randn(Float64, 10,10) + eye(10) # well conditioned
25+
@test inv(SMatrix{10,10}(m))::StaticMatrix inv(m)
26+
27+
28+
# Unsigned versions
829
@test inv(@SMatrix [0x01 0x02; 0x02 0x01])::SMatrix [-1/3 2/3; 2/3 -1/3]
930
@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-
31+
1132
m = triu(randn(Float64, 4,4) + eye(4))
1233
@test inv(SMatrix{4,4}(m))::StaticMatrix inv(m)
1334
m = tril(randn(Float64, 4,4) + eye(4))
1435
@test inv(SMatrix{4,4}(m))::StaticMatrix inv(m)
1536
end
37+
38+
39+
@testset "Matrix inverse 4x4" begin
40+
# A random well-conditioned matrix generated with `rand(1:10,4,4)`
41+
m = Float64[8 4 3 1;
42+
9 1 9 8;
43+
5 0 2 1;
44+
7 5 7 2]
45+
sm = SMatrix{4,4}(m)
46+
@test isapprox(inv(sm)::StaticMatrix, inv(m), rtol=2e-15)
47+
48+
# Poorly conditioned matrix; almost_singular_matrix(4, 3, 1e-7)
49+
m = [
50+
2.83056817904263402e-01 1.26822318692296848e-01 2.59665505365002547e-01 1.24524798964590747e-01
51+
4.29869029098094768e-01 2.29977256378012789e-03 4.63841570354149135e-01 1.51975221831027241e-01
52+
2.33883312850939995e-01 7.69469213907006122e-02 3.16525493661056589e-01 1.06181613512460249e-01
53+
1.63327582123091175e-01 1.70552365412100865e-01 4.55720450934200327e-01 1.23299968650232419e-01
54+
]
55+
sm = SMatrix{4,4}(m)
56+
@test norm(Matrix(sm*inv(sm) - eye(4))) < 10*norm(m*inv(m) - eye(4))
57+
@test norm(Matrix(inv(sm)*sm - eye(4))) < 10*norm(inv(m)*m - eye(4))
58+
59+
# Poorly conditioned matrix, generated by fuzz testing with
60+
# almost_singular_matrix(N, 3, 1e-7)
61+
# Case where the 4x4 algorithm fares badly compared to Base inv
62+
m = [1.80589503332920231e-02 2.37595944073211218e-01 3.80770755039433806e-01 5.70474286930408372e-02
63+
2.17494123374967520e-02 2.94069278741667661e-01 2.57334651147732463e-01 4.02348559939922357e-02
64+
3.42945636322714076e-01 3.29329508494837497e-01 2.25635541033857107e-01 6.03912636987153917e-02
65+
4.77828437366344727e-01 4.86406974015710591e-01 1.95415684569693188e-01 6.74775080892497797e-02]
66+
sm = SMatrix{4,4}(m)
67+
@test_broken norm(Matrix(inv(sm)*sm - eye(4))) < 10*norm(inv(m)*m - eye(4))
68+
@test_broken norm(Matrix(sm*inv(sm) - eye(4))) < 10*norm(m*inv(m) - eye(4))
69+
end
70+
71+
72+
# Fuzz testing for inv()
73+
#=
74+
for i=1:100
75+
N = 4
76+
A = almost_singular_matrix(N, 3, 1e-7)
77+
SA = SMatrix{N,N}(A)
78+
SA_residual = norm(Matrix(SA*inv(SA) - eye(N)))
79+
A_residual = norm(A*inv(A) - eye(N))
80+
if SA_residual/A_residual > 10000
81+
@printf("%10e %.4f\n", SA_residual, SA_residual/A_residual)
82+
println("[")
83+
for i=1:4
84+
for j=1:4
85+
@printf("%.17e ", A[i,j])
86+
end
87+
println()
88+
end
89+
println("]")
90+
end
91+
end
92+
=#

0 commit comments

Comments
 (0)