Skip to content

Commit 72a7117

Browse files
authored
Howell form (#1735)
* Copy Howell form from Hecke * Normalize the pivots to make it unique * Tests * Use `is_zero_entry` * Test with another ring * Rename
1 parent 53f2bec commit 72a7117

File tree

4 files changed

+330
-2
lines changed

4 files changed

+330
-2
lines changed

src/MatrixNormalForms.jl

Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ end
7474

7575
################################################################################
7676
#
77-
# Matrix normal form over (euclidean) rings (hermite_form)
77+
# Matrix normal form over (euclidean) domains (hermite_form)
7878
#
7979
################################################################################
8080

@@ -87,7 +87,7 @@ It is assumed that `base_ring(A)` is euclidean.
8787
# Keyword arguments
8888
* `reduced`: Whether the columns of $H$ which contain a pivot are reduced. The
8989
default is `true`.
90-
* `shape`: Whether $R$ is an upper-right (`:upper`, default) or lower-left
90+
* `shape`: Whether $H$ is an upper-right (`:upper`, default) or lower-left
9191
(`:lower`) echelon form.
9292
* `trim`: By default, $H$ will have as many rows as $A$ and potentially involve
9393
rows only containing zeros. If `trim = true`, these rows are removed, so that
@@ -143,3 +143,251 @@ function hermite_form_with_transformation(A::MatElem{<:RingElement}; reduced::Bo
143143
end
144144
return H, U
145145
end
146+
147+
################################################################################
148+
#
149+
# Matrix normal form over principal ideal rings (howell_form)
150+
#
151+
################################################################################
152+
153+
# Works in theory over any principal ideal ring; internally we require functions
154+
# annihilator, gcdxx and _div_for_howell_form
155+
156+
# Swap rows so that there is a non-zero entry in A[start_row, col].
157+
# Return 0 if this is not possible, 1 if no swapping was necessary and -1
158+
# if rows were swapped.
159+
function _pivot(A::MatElem, start_row::Int, col::Int)
160+
if !is_zero_entry(A, start_row, col)
161+
return 1
162+
end
163+
164+
for j in start_row + 1:nrows(A)
165+
if !is_zero_entry(A, j, col)
166+
swap_rows!(A, j, start_row)
167+
return -1
168+
end
169+
end
170+
171+
return 0
172+
end
173+
174+
function triangularize!(A::MatElem{<:RingElement})
175+
n = nrows(A)
176+
m = ncols(A)
177+
d = one(base_ring(A))
178+
row = 1
179+
col = 1
180+
while row <= nrows(A) && col <= ncols(A)
181+
t = _pivot(A, row, col)
182+
if iszero(t)
183+
col = col + 1
184+
continue
185+
end
186+
d = d*t
187+
for i in (row + 1):nrows(A)
188+
if is_zero_entry(A, i, col)
189+
continue
190+
end
191+
192+
b, q = divides(A[i, col], A[row, col])
193+
194+
if b
195+
for k in col:m
196+
A[i, k] = A[i, k] - q*A[row, k]
197+
end
198+
else
199+
g, s, t, u, v = gcdxx(A[row, col], A[i, col])
200+
201+
for k in col:m
202+
t1 = s*A[row, k] + t*A[i, k]
203+
t2 = u*A[row, k] + v*A[i, k]
204+
A[row, k] = t1
205+
A[i, k] = t2
206+
end
207+
end
208+
end
209+
row = row + 1
210+
col = col + 1
211+
end
212+
return d
213+
end
214+
215+
# Naive version of inplace strong echelon form
216+
# It is assumed that A has more rows then columns.
217+
function strong_echelon_form_naive!(A::MatElem{<:RingElement})
218+
n = nrows(A)
219+
m = ncols(A)
220+
221+
@assert n >= m
222+
223+
triangularize!(A)
224+
225+
T = zero_matrix(base_ring(A), 1, ncols(A))
226+
for j in 1:m
227+
if !is_zero_entry(A, j, j)
228+
# Normalize/canonicalize the pivot
229+
u = canonical_unit(A[j, j])
230+
if !is_one(u)
231+
uinv = inv(u)
232+
for i in j:ncols(A)
233+
A[j, i] = uinv*A[j, i]
234+
end
235+
end
236+
237+
# This is the reduction
238+
for i in 1:j - 1
239+
if is_zero_entry(A, i, j)
240+
continue
241+
end
242+
q = _div_for_howell_form(A[i, j], A[j, j])
243+
for l in i:m
244+
A[i, l] = A[i, l] - q*A[j, l]
245+
end
246+
end
247+
248+
a = annihilator(A[j, j])
249+
250+
for k in 1:m
251+
T[1, k] = a*A[j, k]
252+
end
253+
else
254+
for k in 1:m
255+
T[1, k] = A[j, k]
256+
end
257+
end
258+
259+
for i in j+1:m
260+
261+
if is_zero_entry(T, 1, i)
262+
continue
263+
end
264+
265+
if is_zero_entry(A, i, i)
266+
for k in i:m
267+
T[1, k], A[i, k] = A[i, k], T[1, k]
268+
end
269+
else
270+
b, q = divides(T[1, i], A[i, i])
271+
if b
272+
for k in i:m
273+
T[1, k] = T[1, k] - q*A[i, k]
274+
end
275+
else
276+
g, s, t, u, v = gcdxx(A[i, i], T[1, i])
277+
for k in i:m
278+
t1 = s*A[i, k] + t*T[1, k]
279+
t2 = u*A[i, k] + v*T[1, k]
280+
A[i, k] = t1
281+
T[1, k] = t2
282+
end
283+
end
284+
end
285+
end
286+
end
287+
return A
288+
end
289+
290+
function howell_form!(A::MatElem{<:RingElement})
291+
@assert nrows(A) >= ncols(A)
292+
293+
strong_echelon_form_naive!(A)
294+
295+
# Move the zero rows to the bottom
296+
for i in 1:nrows(A)
297+
if !is_zero_row(A, i)
298+
continue
299+
end
300+
301+
j = findfirst(l -> !is_zero_row(A, l), i + 1:nrows(A))
302+
if isnothing(j)
303+
break
304+
end
305+
swap_rows!(A, i, i + j)
306+
end
307+
return A
308+
end
309+
310+
@doc raw"""
311+
howell_form(A::MatElem{<:RingElement}; reduced::Bool = true, shape::Symbol = :upper, trim::Bool = false)
312+
313+
Return a Howell form $H$ of $A$.
314+
It is assumed that `base_ring(A)` is a principal ideal ring.
315+
316+
# Keyword arguments
317+
* `reduced`: Whether the columns of $H$ which contain a pivot are reduced. The
318+
default is `true`.
319+
* `shape`: Whether $H$ is an upper-right (`:upper`, default) or lower-left
320+
(`:lower`) echelon form.
321+
* `trim`: By default, $H$ will have at least as many rows as $A$ and potentially
322+
involve rows only containing zeros. If `trim = true`, these rows are removed.
323+
324+
See also [`howell_form_with_transformation`](@ref).
325+
"""
326+
function howell_form(A::MatElem{<:RingElement}; reduced::Bool = true, shape::Symbol = :upper, trim::Bool = false)
327+
if shape !== :upper && shape !== :lower
328+
throw(ArgumentError("Unsupported argument :$shape for shape: Must be :upper or :lower."))
329+
end
330+
331+
H = deepcopy(A)
332+
if shape === :lower
333+
H = reverse_cols!(H)
334+
end
335+
336+
if nrows(H) < ncols(H)
337+
H = vcat(H, zero_matrix(base_ring(H), ncols(H) - nrows(H), ncols(H)))
338+
end
339+
340+
howell_form!(H)
341+
342+
r = nrows(H)
343+
# Compute the rank (if necessary)
344+
if trim
345+
while r > 0 && is_zero_row(H, r)
346+
r -= 1
347+
end
348+
H = sub(H, 1:r, 1:ncols(H))
349+
end
350+
351+
if shape === :lower
352+
reverse_cols!(H)
353+
reverse_rows!(H)
354+
end
355+
return H
356+
end
357+
358+
@doc raw"""
359+
howell_form_with_transformation(A::MatElem{<:RingElement}; reduced::Bool = true, shape::Symbol = :upper)
360+
361+
Return a Howell form $H$ of $A$ and a matrix $U$ with $H = UA$.
362+
Notice that $H$ may have more rows than $A$ and hence $U$ may not be invertible.
363+
It is assumed that `base_ring(A)` is a principal ideal ring
364+
365+
See [`hermite_form`](@ref) for the keyword arguments.
366+
"""
367+
function howell_form_with_transformation(A::MatElem{<:RingElement}; reduced::Bool = true, shape::Symbol = :upper)
368+
if shape !== :upper && shape !== :lower
369+
throw(ArgumentError("Unsupported argument :$shape for shape: Must be :upper or :lower."))
370+
end
371+
372+
if shape === :lower
373+
B = hcat(reverse_cols(A), identity_matrix(A, nrows(A)))
374+
else
375+
B = hcat(A, identity_matrix(A, nrows(A)))
376+
end
377+
if nrows(B) < ncols(B)
378+
B = vcat(B, zero(A, ncols(B) - nrows(B), ncols(B)))
379+
end
380+
381+
howell_form!(B)
382+
383+
m = max(nrows(A), ncols(A))
384+
H = sub(B, 1:m, 1:ncols(A))
385+
U = sub(B, 1:m, ncols(A) + 1:ncols(B))
386+
387+
if shape === :lower
388+
reverse_cols!(H)
389+
reverse_rows!(H)
390+
reverse_rows!(U)
391+
end
392+
return H, U
393+
end

src/Residue.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ function is_zero_divisor_with_annihilator(a::ResElem)
9696
return !is_unit(g), parent(a)(b)
9797
end
9898

99+
annihilator(a::ResElem) = is_zero_divisor_with_annihilator(a)[2]
100+
99101
deepcopy_internal(a::ResElem, dict::IdDict) =
100102
parent(a)(deepcopy_internal(data(a), dict))
101103

@@ -379,6 +381,32 @@ function gcd(a::ResElem{T}, b::ResElem{T}) where {T <: RingElement}
379381
return parent(a)(gcd(gcd(data(a), modulus(a)), data(b)))
380382
end
381383

384+
# Return g, s, t, u, v in R with sv - tu a unit and
385+
# [s t] [a] = [g]
386+
# [u v] [b] [0]
387+
# Generic implementation which uses HNF over the base ring.
388+
# g might not coincide with gcd(a, b) because gcd(a, b) is
389+
# gcd(gcd(data(a), modulus(a)), data(b)) and g is just
390+
# gcd(data(a), data(b)).
391+
function gcdxx(a::ResElem{T}, b::ResElem{T}) where {T <: RingElement}
392+
check_parent(a, b)
393+
R = parent(a)
394+
M = matrix(base_ring(R), 2, 1, [data(a), data(b)])
395+
H, U = hermite_form_with_transformation(M)
396+
@assert is_zero(H[2, 1])
397+
return R(H[1, 1]), R(U[1, 1]), R(U[1, 2]), R(U[2, 1]), R(U[2, 2])
398+
end
399+
400+
# The operation "Quo" on p. 13 of Storjohann "Algorithms for matrix canonical forms"
401+
function _div_for_howell_form(a::ResElem{T}, b::ResElem{T}) where {T <: RingElement}
402+
check_parent(a, b)
403+
return parent(a)(div(data(a), data(b)))
404+
end
405+
406+
# Fallback for euclidean rings (that is, rings implementing the euclidean ring
407+
# interface)
408+
_div_for_howell_form(a::T, b::T) where {T <: RingElement} = div(a, b)
409+
382410
###############################################################################
383411
#
384412
# Unsafe functions

src/exports.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ export hnf_with_transform
243243
export hom
244244
export hom_direct_sum
245245
export hooklength
246+
export howell_form
247+
export howell_form_with_transformation
246248
export ideal
247249
export identity_map
248250
export identity_matrix

test/MatrixNormalForms-test.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
R = reverse_rows(reverse_cols(S))
2424
@test is_rref(R)
2525
@test U*M == S
26+
@test is_invertible(U)
2627
end
2728

2829
@testset "Hermite form" begin
@@ -50,4 +51,53 @@ end
5051
H = reverse_rows(reverse_cols(Hl))
5152
@test is_hnf(H)
5253
@test U*M == Hl
54+
@test is_invertible(U)
55+
end
56+
57+
@testset "Howell form" begin
58+
R, _ = residue_ring(ZZ, 30)
59+
M = R[2 3 4 5; 6 9 12 15; 10 15 20 30]
60+
H = howell_form(M)
61+
@test H == R[2 3 4 0; 0 15 0 0; 0 0 0 5; 0 0 0 0]
62+
Hl = howell_form(M, shape = :lower)
63+
@test Hl == R[0 0 0 0; 0 15 0 0; 16 9 2 0; 0 0 0 5]
64+
H = howell_form(M, trim = true)
65+
@test H == R[2 3 4 0; 0 15 0 0; 0 0 0 5]
66+
Hl = howell_form(M, shape = :lower, trim = true)
67+
@test Hl == R[0 15 0 0; 16 9 2 0; 0 0 0 5]
68+
69+
H, U = howell_form_with_transformation(M)
70+
@test H == R[2 3 4 0; 0 15 0 0; 0 0 0 5; 0 0 0 0]
71+
@test U*M == H
72+
Hl, U = howell_form_with_transformation(M, shape = :lower)
73+
@test Hl == R[0 0 0 0; 0 15 0 0; 16 9 2 0; 0 0 0 5]
74+
@test U*M == Hl
75+
76+
R, _ = residue_ring(ZZ, 12)
77+
M = R[4 1 0; 3 0 0; 0 0 5]
78+
@test howell_form(M) == R[1 1 0; 0 3 0; 0 0 1]
79+
80+
a = R[4 1 0; 0 0 5; 0 0 0]
81+
b = R[8 5 5; 0 9 8; 0 0 10]
82+
c = R[4 1 0; 0 3 0; 0 0 1]
83+
@test howell_form(a) == c
84+
@test howell_form(b) == c
85+
86+
Qt, t = QQ["t"]
87+
R, _ = residue_ring(Qt, t^5)
88+
M = R[t^2 t^2 - 1 1; t t - 1 0]
89+
H = howell_form(M)
90+
@test H == R[t 0 -1; 0 1 -t^3 - t^2 - t - 1; 0 0 t^4]
91+
H = howell_form(M, shape = :lower)
92+
@test H == R[0 0 0; -t^4 - t^3 - t^2 - t 1 0; -t 0 1]
93+
H = howell_form(M, trim = true)
94+
@test H == R[t 0 -1; 0 1 -t^3 - t^2 - t - 1; 0 0 t^4]
95+
H = howell_form(M, shape = :lower, trim = true)
96+
@test H == R[-t^4 - t^3 - t^2 - t 1 0; -t 0 1]
97+
H, U = howell_form_with_transformation(M)
98+
@test H == R[t 0 -1; 0 1 -t^3 - t^2 - t - 1; 0 0 t^4]
99+
@test U*M == H
100+
H, U = howell_form_with_transformation(M, shape = :lower)
101+
@test H == R[0 0 0; -t^4 - t^3 - t^2 - t 1 0; -t 0 1]
102+
@test U*M == H
53103
end

0 commit comments

Comments
 (0)