Skip to content

Commit 05c9155

Browse files
Add the Jordan canonical form
This enables ```julia julia> using MatrixFactorizations julia> A = [0 0 1 7 -1; -5 -6 -6 -35 5; 1 1 -7 7 -1; 0 0 0 -9 0; 2 1 -5 -42 -3] 5×5 Matrix{Int64}: 0 0 1 7 -1 -5 -6 -6 -35 5 1 1 -7 7 -1 0 0 0 -9 0 2 1 -5 -42 -3 julia> λ = [-9, -7, -7, -1, -1//1] 5-element Vector{Rational{Int64}}: -9 -7 -7 -1 -1 julia> V, J = jordan(A, λ) Jordan{Rational{Int64}, Matrix{Rational{Int64}}, Matrix{Rational{Int64}}} Generalized eigenvectors: 5×5 Matrix{Rational{Int64}}: 0 0 0 1 0 0 1 0 0 -1 0 1 -1 0 0 1 0 0 0 0 7 1 -1 1 -1 Jordan normal form: 5×5 Matrix{Rational{Int64}}: -9 0 0 0 0 0 -7 1 0 0 0 0 -7 0 0 0 0 0 -1 1 0 0 0 0 -1 julia> A*V == V*J true ``` Note that Jordan blocks are unstable with respect to perturbations in the matrix, so the code is generally best used with rational arithmetic (with matrices with rational eigenvalues), though general support is provided. In the future, the Jordan normal form of the matrix could benefit from structuring as a `BlockDiagonal` matrix with a new type of `LayoutMatrix` to describe a `JordanBlock`, but this comes with the drawback of including other packages in the requirements. `exp(J::JordanBlock)` (and other matrix functions) is also an `UpperTriangularToeplitz` matrix, though this gets quite esoteric.
1 parent cbc6178 commit 05c9155

File tree

3 files changed

+553
-1
lines changed

3 files changed

+553
-1
lines changed

src/MatrixFactorizations.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import ArrayLayouts: reflector!, reflectorApply!, materialize!, @_layoutlmul, @_
3434

3535
export ul, ul!, ql, ql!, qrunblocked, qrunblocked!, UL, QL,
3636
reversecholesky, reversecholesky!, ReverseCholesky,
37-
lulinv, lulinv!, LULinv
37+
lulinv, lulinv!, LULinv, jordan, Jordan
3838

3939
const AdjointQtype = isdefined(LinearAlgebra, :AdjointQ) ? LinearAlgebra.AdjointQ : Adjoint
4040
const AbstractQtype = AbstractQ <: AbstractMatrix ? AbstractMatrix : AbstractQ
@@ -125,5 +125,6 @@ include("rq.jl")
125125
include("polar.jl")
126126
include("reversecholesky.jl")
127127
include("lulinv.jl")
128+
include("jordan.jl")
128129

129130
end #module

src/jordan.jl

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
"""
2+
Jordan <: Factorization
3+
4+
Jordan canonical form of a square matrix `A = VJV⁻¹`. This
5+
is the return type of [`jordan`](@ref), the corresponding Jordan factorization function.
6+
7+
The individual components of the factorization `F::Jordan` can be accessed via [`getfield`](@ref):
8+
9+
| Component | Description |
10+
|:----------|:-----------------------------|
11+
| `F.V` | `V` generalized eigenvectors |
12+
| `F.J` | `J` Jordan normal form |
13+
14+
Iterating the factorization produces the components `F.V` and `F.J`.
15+
16+
# Examples
17+
```jldoctest
18+
julia> A = [5 4 2 1; 0 1 -1 -1; -1 -1 3 0; 1 1 -1 2//1]
19+
20+
julia> λ = [1, 2, 4, 4//1] # you will almost certainly need extremely accurate eigenvalues to proceed
21+
22+
julia> F = jordan(A, λ)
23+
Jordan{Rational{Int64}, Matrix{Rational{Int64}}, Matrix{Rational{Int64}}}
24+
Generalized eigenvectors:
25+
4×4 Matrix{Rational{Int64}}:
26+
1 1 -1 -1
27+
-1 -1 0 0
28+
0 0 1 0
29+
0 1 -1 0
30+
Jordan normal form:
31+
4×4 Matrix{Rational{Int64}}:
32+
1 0 0 0
33+
0 2 0 0
34+
0 0 4 1
35+
0 0 0 4
36+
37+
julia> A*F.V == F.V*F.J
38+
true
39+
40+
julia> V, J = jordan(A, λ); # destructuring via iteration
41+
42+
julia> V == F.V && J == F.J
43+
true
44+
```
45+
"""
46+
struct Jordan{T, R <: AbstractMatrix{T}, S <: AbstractMatrix{T}} <: Factorization{T}
47+
V::R
48+
J::S
49+
end
50+
51+
Jordan{T}(V::AbstractMatrix, J::AbstractMatrix) where T = Jordan(convert(AbstractMatrix{T}, V), convert(AbstractMatrix{T}, J))
52+
Jordan{T}(F::Jordan) where T = Jordan{T}(F.V, F.J)
53+
54+
iterate(F::Jordan) = (F.V, Val(:J))
55+
iterate(F::Jordan, ::Val{:J}) = (F.J, Val(:done))
56+
iterate(F::Jordan, ::Val{:done}) = nothing
57+
58+
function show(io::IO, mime::MIME{Symbol("text/plain")}, F::Jordan)
59+
summary(io, F); println(io)
60+
println(io, "Generalized eigenvectors:")
61+
show(io, mime, F.V)
62+
println(io, "\nJordan normal form:")
63+
show(io, mime, F.J)
64+
end
65+
66+
# dangerous because Jordan blocks are unstable with respect to small perturbations
67+
jordan(A::AbstractMatrix) = jordan(A, eigvals(A))
68+
69+
function jordan(A::AbstractMatrix{S}, λ::AbstractVector{T}) where {S, T}
70+
V = promote_type(S, T)
71+
jordan(convert(AbstractMatrix{V}, A), convert(AbstractVector{V}, λ))
72+
end
73+
74+
function jordan(A::AbstractMatrix{T}, λ::AbstractVector{T}) where T
75+
PLEP, B = block_diagonalize(A, λ)
76+
F, J = block_diagonal_to_jordan(B)
77+
V = PLEP*F
78+
return Jordan(V, J)
79+
end
80+
81+
function triangular_to_psychologically_block_diagonal(U::UpperTriangular{T, <: AbstractMatrix{T}}) where T
82+
n = checksquare(U)
83+
R = deepcopy(U)
84+
EACC = UpperTriangular(Matrix{T}(I, n, n))
85+
# note that this is sensitive to the order of conjugation.
86+
for j in 2:n
87+
for i in j-1:-1:1
88+
if R[i, i] != R[j, j]
89+
E = UpperTriangular(Matrix{T}(I, n, n))
90+
E[i, j] = R[i, j]/(R[j, j] - R[i, i])
91+
R = E\R*E
92+
EACC = EACC*E
93+
end
94+
end
95+
end
96+
return EACC, R
97+
end
98+
99+
function psychologically_block_diagonal_to_block_diagonal(R::UpperTriangular{T, <: AbstractMatrix{T}}) where T
100+
p = sortperm(diag(R))
101+
n = length(p)
102+
P = Matrix{Int}(I, n, n)[:, p]
103+
B = R[p, p]
104+
P, B
105+
end
106+
107+
function triangular_to_block_diagonal(U::UpperTriangular{T, <: AbstractMatrix{T}}) where T
108+
E, R = triangular_to_psychologically_block_diagonal(U)
109+
p = sortperm(diag(R))
110+
E[:, p], R[p, p]
111+
end
112+
113+
function block_diagonalize(A::AbstractMatrix{T}, λ::AbstractVector{T}) where T
114+
F = lulinv(A, λ)
115+
PLEP, B = triangular_to_block_diagonal(UpperTriangular(F.factors))
116+
lmul!(UnitLowerTriangular(F.factors), PLEP)
117+
F.P*PLEP, B
118+
end
119+
120+
function determine_block_sizes(B::AbstractMatrix{T}) where T
121+
n = checksquare(B)
122+
if n == 1
123+
m = [1]
124+
return m
125+
else
126+
m = Int[]
127+
i = 1
128+
while i < n
129+
t = 1
130+
while (i < n-1) && (B[i, i] == B[i+1, i+1])
131+
i += 1
132+
t += 1
133+
end
134+
if i == n-1
135+
if B[i, i] == B[i+1, i+1]
136+
t += 1
137+
push!(m, t)
138+
else
139+
push!(m, t)
140+
push!(m, 1)
141+
end
142+
else
143+
push!(m, t)
144+
end
145+
i += 1
146+
end
147+
return m
148+
end
149+
end
150+
151+
function block_diagonal_to_jordan(B::Matrix{T}) where T
152+
n = checksquare(B)
153+
m = determine_block_sizes(B)
154+
cm = cumsum(m)
155+
pushfirst!(cm, 0)
156+
F = zeros(T, n, n)
157+
J = zeros(T, n, n)
158+
for i in 1:length(m)
159+
ir = cm[i]+1:cm[i+1]
160+
FB, JB = upper_triangular_block_to_jordan_blocks(B[ir, ir])
161+
F[ir, ir] .= FB
162+
J[ir, ir] .= JB
163+
end
164+
return F, J
165+
end
166+
167+
# Contract: B_{i, j} = 0 for i > j. B_{i, i} = B_{j, j} for all i ≠ j.
168+
function upper_triangular_block_to_jordan_blocks(B::Matrix{T}) where T
169+
n = checksquare(B)
170+
J = deepcopy(B)
171+
FACC = Matrix{T}(I, n, n)
172+
for j in 2:n
173+
# In column j, we shall introduce zeros in any row with a 1 in the {i, i+1} position.
174+
F = Matrix{T}(I, n, n)
175+
for i in 1:j-2
176+
if !iszero(J[i, i+1])
177+
F[i+1, j] = -J[i, j]
178+
end
179+
end
180+
J = F\J*F
181+
FACC = FACC*F
182+
while count(!iszero, J[1:j-1, j]) > 1
183+
# Next, we identify the first and next nonzeros in the last column. By the first step, they are across from the last row of a Jordan block.
184+
i1 = 1
185+
while i1 < j
186+
if !iszero(J[i1, j])
187+
break
188+
else
189+
i1 += 1
190+
end
191+
end
192+
i2 = i1+1
193+
while i2 < j
194+
if !iszero(J[i2, j])
195+
break
196+
else
197+
i2 += 1
198+
end
199+
end
200+
# With i1 and i2, we find the sizes of the corresponding Jordan blocks, s and t.
201+
i1s = i1
202+
while i1s > 1
203+
if iszero(J[i1s-1, i1s])
204+
break
205+
else
206+
i1s -= 1
207+
end
208+
end
209+
i2s = i2
210+
while i2s > 1
211+
if iszero(J[i2s-1, i2s])
212+
break
213+
else
214+
i2s -= 1
215+
end
216+
end
217+
i1r = i1s:i1
218+
i2r = i2s:i2
219+
s = length(i1r)
220+
t = length(i2r)
221+
# Eliminate one of the nonzeros or the other.
222+
if s t
223+
# eliminate α
224+
F = Matrix{T}(I, n, n)
225+
α = J[i1, j]
226+
β = J[i2, j]
227+
γ = α/β
228+
F[i1r, i2-s+1:i2] .= Matrix{T}*I, s, s)
229+
J = F\J*F
230+
FACC = FACC*F
231+
else
232+
# eliminate β
233+
F = Matrix{T}(I, n, n)
234+
α = J[i1, j]
235+
β = J[i2, j]
236+
γ = β/α
237+
F[i2r, i1-t+1:i1] .= Matrix{T}*I, t, t)
238+
J = F\J*F
239+
FACC = FACC*F
240+
end
241+
end
242+
# Next, we must permute to get the final nonzero to the bottom, if there is one at all.
243+
i1 = 1
244+
while i1 < j
245+
if !iszero(J[i1, j])
246+
break
247+
else
248+
i1 += 1
249+
end
250+
end
251+
if i1 < j-1
252+
# With i1, we find the size of the corresponding Jordan block, s.
253+
i1s = i1
254+
while i1s > 1
255+
if iszero(J[i1s-1, i1s])
256+
break
257+
else
258+
i1s -= 1
259+
end
260+
end
261+
i1r = i1s:i1
262+
s = length(i1r)
263+
p = [1:i1s-1; i1+1:j-1; i1r; j:n]
264+
#P = Matrix{T}(I, n, n)[:, p]
265+
#J = P'J*P
266+
#FACC = FACC*P
267+
J = J[p, p]
268+
FACC = FACC[:, p]
269+
end
270+
# Finally, we must scale off the nonzero entry to 1 to get it to conform to a Jordan block.
271+
if !iszero(J[j-1, j])
272+
F = Matrix{T}(I, n, n)
273+
F[j, j] = inv(J[j-1, j])
274+
J = F\J*F
275+
FACC = FACC*F
276+
end
277+
end
278+
return FACC, J
279+
end

0 commit comments

Comments
 (0)