Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Vinberg's algorithm #4023

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/doc.main
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"Hecke/manual/quad_forms/genusherm.md",
"Hecke/manual/quad_forms/integer_lattices.md",
"Hecke/manual/quad_forms/Zgenera.md",
"Hecke/manual/quad_forms/discriminant_group.md"
"Hecke/manual/quad_forms/discriminant_group.md",
"LinearAlgebra/vinberg.md",
],
],

Expand Down
18 changes: 18 additions & 0 deletions docs/oscar_references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2261,6 +2261,13 @@ @Article{Tra04
doi = {10.1016/j.cagd.2004.07.001}
}

@PhDThesis{Tur17,
author = {Turkalj, I.},
title = {Reflective Lorentzian lattices of signature (5, 1)},
year = {2017},
school = {TU Dortmund}
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
}

@Misc{VE22,
author = {Vaughan-Lee, Michael and Eick, Bettina},
title = {SglPPow, Database of groups of prime-power order for some prime-powers, Version 2.3},
Expand All @@ -2270,6 +2277,17 @@ @Misc{VE22
url = {https://gap-packages.github.io/sglppow/}
}

@InCollection{Vin75,
author = {Vinberg, E. B.},
title = {Some arithmetical discrete groups in Lobacevskii spaces},
booktitle = {Discrete subgroups of Lie groups and applications to moduli (Internat. Colloq., Bombay, 1973)},
series = {Tata Inst. Fundam. Res. Stud. Math.},
volume = {No. 7},
pages = {323--348},
publisher = {Published for the Tata Institute of Fundamental Research, Bombay by Oxford University Press, Bombay},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, did you run bibtool as described here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if there is a DOI available, please add it as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a DOI.

year = {1975}
}

@MastersThesis{Vol23,
author = {Volz, Sebastian},
title = {Design and implementation of efficient algorithms for operations on partitions of sets},
Expand Down
52 changes: 52 additions & 0 deletions docs/src/LinearAlgebra/vinberg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
```@meta
CurrentModule = Oscar
DocTestSetup = Oscar.doctestsetup()
```

# Vinberg's algorithm

A Lorentzian lattice $L$ is an integral $\Z$-lattice of signature $(s_+, s_-)$ with $s_+=1$ and $s_->0$.
$L$ is called reflective if the set of fundamental roots $\~{R}(L)$ is finite.

See for example [Tur17](@cite) for the theory of Arithmetic Reflection Groups and Reflective Lorentzian Lattices.

## Description
The algorithm constructs a fundamental polyhedron $P$ for a Lorentzian lattice $L$ by computing its fundamental roots $r$.

Choose $v_0$ in $L$ with $v_0^2 > 0$ as a point that $P$ should contain.

Let $Q$ be the corresponding gram matrix of $L$. A fundamental root $r$ satisfies
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
- the vector $r$ is primitive
- reflection by $r$ preserves the lattice, i.e. $\frac{2}{r^2}*r*Q$ is an integer matrix.
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
- the pair $(r, v_0)$ is positive oriented, i.e. $(r, v_0) > 0$
- the product $(r, \~{r}) \geq \ 0$ for all roots $\~{r}$ already found
This implies that $r^2$ divides $2*i$ for $i$ being the level of $Q$, i.e. the last invariant of the Smith normal form of $Q$.

$P$ can be constructed by solving $(r, v_0) = n$ and $r^2 = k$ by increasing order of the value $\frac{n^2}{k}$ and $r$ satisfying the above conditions.

If $v_0$ lies on a root hyperplane, then $P$ is not uniquely determined.
In that case we need a direction vector $v_1$ which satisfies $(\~{v}, v_1) \neq 0$
for all possible roots $\~{v}$ with $(v_0, \~{v}) = 0$

With $v_0$ and $v_1$ fixed $P$ is uniquely determined for any choice of root lengths and maximal distance $(v_0, r)$.
We choose the first roots $r$ by increasing order of the value $\frac{(\~{r}, v_1)}{r^2}$ for all possible roots $\~{v}$ with $(v_0, \~{v}) = 0$.
For any other root length we continue as stated above.

For proofs of the statements above and further explanations see [Vin75](@cite).

## Function

```@docs
vinberg_algorithm(Q::ZZMatrix, upper_bound::ZZRingElem)
```


## Contact

Please direct questions about this part of OSCAR to the following people:
* [Simon Brandhorst](https://www.math.uni-sb.de/ag/brandhorst/index.php?lang=en).
* [Stevell Muller](https://www.math.uni-sb.de/ag/brandhorst/index.php?option=com_content&view=article&id=30:muller&catid=10&lang=de&Itemid=104),

You can ask questions in the [OSCAR Slack](https://www.oscar-system.org/community/#slack).

Alternatively, you can [raise an issue on github](https://www.oscar-system.org/community/#how-to-report-issues).
291 changes: 291 additions & 0 deletions src/NumberTheory/vinberg.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
###################################################################
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it confusing that the source code for this is in NumberTheory but the documentation in LinearAlgebra. I would rather keep those in sync. If this belongs to LinearAlgebra, then let's add a src/LinearAlgebra/ dir?

Then again, should this even go directly into src, or perhaps first into experimental? There are many things still a bit rough and unclear to me, e.g. the use of matrices to represent lattices.

# Vinberg's algorithm
###################################################################

@doc raw"""
_check_v0(Q::ZZMatrix, v0::ZZMatrix) -> ZZMatrix

Checks if the inner product of the given `v0` is positive or generates such a `v0` if none is given.
"""
function _check_v0(Q::ZZMatrix, v0::ZZMatrix)
if iszero(v0)
l = nrows(Q)
for signal in 1:100000000
v0_ = rand(-30:30, l)
if !_is_primitive(v0_)
continue
end
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
v0 = matrix(ZZ, 1, l, v0_)
if (v0*Q*transpose(v0))[1, 1] > 0
return v0
end
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
end
error("Choose another Q")
else
@req (v0*Q*transpose(v0))[1, 1] > 0 "v0 has non positive inner product"
@req isone(reduce(gcd, v)) "v0 is not primitive"
return v0
end
end

@doc raw"""
_all_root_lengths(Q::ZZMatrix) -> Vector{ZZRingElem}

If the user does not want any specific root lengths, we take all of them.

For `Q` the matrix representing the hyperbolic reflection lattice and `l` a length to check,
it is necessary that `l` divides $2.i$ for `l` being a possible root length,
with `i` being the level of `Q`, i.e. the last invariant (biggest entry) of the Smith normal form of `Q`.
Prf: `r` being a root implies that $\frac{2.r.L}{r^2}$ is a subset of $\Z$
`r` being primitive implies that $2.i.\Z$ is a subset of $2.r.L$,
which is a subset of $(r^2).\Z$, what implies that $r^2$ divides $2.i$
"""
function _all_root_lengths(Q::ZZMatrix)
l = nrows(Q)
S = snf(Q)
return (-1) * divisors(2 * S[l, l])
end

@doc raw"""
_check_root_lengths(Q::ZZMatrix, root_lengths::Vector{ZZRingElem}) -> Vector{ZZRingElem}

Check whether the given lengths are possible root lengths.
Unnecessary roots are sorted out and reported if this is wanted.
Return only the possible root lengths.
"""
function _check_root_lengths(Q::ZZMatrix, root_lengths::Vector{ZZRingElem})
l = nrows(Q)
S = snf(Q)
possible_root_lengths = divisors(2 * S[l, l])
if isempty(root_lengths)
return possible_root_lengths
end
result = ZZRingElem[]
for r in root_lengths
if abs(r) in possible_root_lengths
push!(result, r)
else
@vprintln :Vinberg 1 "$r is not a possible root length"
end
end
@req !isempty(result) "No possible root lengths found"
return result
end

@doc raw"""
_distance_indices(upper_bound::ZZRingElem, root_lengths::Vector{ZZRingElem}) -> Vector{Tuple{Int, ZZRingElem}}

Returns all possible tuples $(n, l)$ with `n` a natural number and `l` contained in `root_lengths`,
sorted by increase of the value $\frac{n^2}{l}$, stopping by `upper_bound`.
"""
function _distance_indices(upper_bound::Rational, root_lengths::Vector{ZZRingElem})

result = Tuple{Int,ZZRingElem}[]

for l in root_lengths
x = isqrt(x0*upper_bound) # consider n^2//l < upper_bound => n < sqrt(l*upper_bound)
for n in 1:x
push!(result, (n, l))
end
end

sort!(result; by=x -> (x[1]^2) // abs(x[2]))
return result
end

@doc raw"""
_is_primitive(v)
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved

Check if `v` is primitive, which is equivalent to checking whether the greatest common divisor of the entries of `v`
is equal to 1. If `v` is not primitive, we divide `v` by its gcd.
"""
function _is_primitive(v)
gcd = reduce(gcd, v)
if isone(gcd)
return v
else
return (1//gcd * v)
end
end

@doc raw"""
_crystallographic_condition(Q::ZZMatrix, v::ZZMatrix)

Check if reflection by `v` preserves the lattice, i.e. check if for `v` a row vector
$\frac{2.v.Q}{v^2}$ is an integer matrix.
"""
function _crystallographic_condition(Q::ZZMatrix, v::ZZMatrix)
A = Q * transpose(v)
b = (v*A)[1, 1]
A = 2 * A

return all(iszero(mod(x, b)) for x in A)
end

@doc raw"""
_check_coorientation(Q::ZZMatrix, roots::Vector{ZZMatrix}, v::ZZMatrix, v0::ZZMatrix)

First check whether `v` has non-obtuse angles with all roots `r` already found by checking that $v.r \geq 0$.
Then also check the coorientation of `v` by checking that $v.v_0 \geq 0$.
"""
function _check_coorientation(Q::ZZMatrix, roots::Vector{ZZMatrix}, v::ZZMatrix, v0::ZZMatrix)
Qv = Q * transpose(v)
for r in roots
if (r*Qv)[1, 1] < 0
return false
end
end

return (v*Q*transpose(v0))[1, 1] > 0
end

@doc raw"""
_distance_0(Q::ZZMatrix, v0::ZZMatrix, root_lengths::Vector{ZZRingElem}) -> Vector{ZZMatrix}

Return the roots which are orthogonal to `v0`.
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved

After gathering all primitive vectors `v` which are orthogonal to `v0` with length contained in `root_lengths`
satisfying the crystallographic condition, we check for those `v` in order of increase of the value $\frac{(v.v_1)^2}{v^2}$
if they are roots, e.g. if they also satisfy
- $(v.v_1) > 0$
- $(v.r) \geq 0$ for all roots `r` already found
"""
function _distance_0(Q::ZZMatrix, v0::ZZMatrix, root_lengths::Vector{ZZRingElem}, direction_vector::ZZMatrix)
roots = ZZMatrix[]
possible_vec = ZZMatrix[]
# _short_vectors_gram is more efficient than short_vectors_affine
# Thus we have to make some preparations
bm = kernel(Q*transpose(v0); side=:left)
QI = bm*Q*transpose(bm)
QI = QI[1, 1] > 0 ? QI : -QI
for d in root_lengths # gather all vectors which are orthogonal on v0 with length contained in root_lengths
d = abs(d)
V = Hecke._short_vectors_gram(Hecke.LatEnumCtx, map_entries(QQ, QI), d, d, ZZRingElem) # QI POSITIVE
for (v_, _) in V
v = matrix(ZZ, 1, nrows(bm), v_) * bm
if !_is_primitive(v)
continue
end
if _crystallographic_condition(Q, v)
push!(possible_vec, v)
end
end
end
is_empty(possible_vec) && return roots
# we check if we can take the given direction vector or generate one if there is no one given
if iszero(direction_vector)
v1 = _generate_direction_vector(Q, v0, possible_vec)
else
v1 = _check_direction_vector(Q, v0, possible_vec, direction_vector)
end
# Now we want to sort the vectors by increase of the value ((v.v1)^2)//v^2
possible_vec_sort = Tuple{ZZMatrix,RationalUnion,RationalUnion}[]
for v in possible_vec
vQ = v * Q
n = (vQ*v1)[1, 1]
k = (vQ*transpose(v))[1, 1]
push!(possible_vec_sort, (v, n, k))
end
sort!(possible_vec_sort; by=x -> (x[2]^2) // abs(x[3]))

# We execute the algorithm with replacing v0 by v1
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
for (v, n) in possible_vec_sort
if n < 0
v = -v
end
vQ = v*Q
if all((vQ*transpose(w))[1, 1] >= 0 for w in roots)
push!(roots, v)
end
end
return roots
end

@doc raw"""
_generate_direction_vector(Q::ZZMatrix, v0::ZZMatrix, possible_vec::Vector{ZZMatrix}) -> ZZMatrix

Since no direction vector `v1` was given, we need to find one which satisfies $(\~{v}, v_1) \neq 0$
for all possible roots $\~{v}$ with $(v_0, \~{v}) = 0$
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
"""
function _generate_direction_vector(Q::ZZMatrix, v0::ZZMatrix, possible_vec::Vector{ZZMatrix})
l = length(v0)
v1 = zero_matrix(QQ, l, 1) # initializing v1 that it survives the for loop
signal = 1 # initializing a stopping condition
while signal in 1:100000000
if l < 4
v1_ = rand(-30:30, l)
elseif l < 10
v1_ = rand(-25:25, l)
else
v1_ = rand(-20:20, l) # for higher l it is not necessary/efficient to choose a big random range
end
v1 = matrix(QQ, l, 1, v1_)
@hassert :Vinberg 1 (transpose(v1)*Q*v1)[1, 1] < 0
if all((v*Q*v1)[1, 1] != 0 for v in possible_vec)
# To run the algorithm it suffices to require that v*Q*v1 != 0, but we get a random (right) solution.
# This is the case because it is a random choice in which direction the fundamental cone
# is built everytime we use the algorithm.
# To get always the same solution one should suply v1
@vprintln :Vinberg 2 "direction vector v1 = $v1"
return v1
end
signal += 1
end
@req signal < 10000000 "Choose another v0" # break the algorithm if no v1 was found
end

@doc raw"""
_check_direction_vector(Q::ZZMatrix, v0::ZZMatrix, possible_vec::Vector{ZZMatrix}, direction_vector::ZZMatrix) -> ZZMatrix

We check if the given direction vector `v1` satisfies $(\~{v}, v_1) \neq 0$
for all possible roots $\~{v}$ with $(v_0, \~{v}) = 0$
"""
function _check_direction_vector(Q::ZZMatrix, v0::ZZMatrix, possible_vec::Vector{ZZMatrix}, direction_vector::ZZMatrix)
v1 = transpose(direction_vector)
@req all((v*Q*v1)[1, 1] != 0 for v in possible_vec) "The direction vector cannot be orthogonal to one of the possible roots"
return v1
end

@doc raw"""
vinberg_algorithm(Q::ZZMatrix, upper_bound::ZZRingElem; v0::ZZMatrix, root_lengths::Vector{ZZRingElem}, direction_vector::ZZMatrix) -> Vector{ZZMatrix}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q is a matrix, not a lattice, though the text below refers to it as "a lattice". This always opens up ambiguities as to how to interpret that matrix. I thought we had types specifically to represent lattices, such as ZZLat, why not use that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I agree with fingolfin please prepare a dispatch via ZZLat.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElMerkel You marked this as resolved, but I cannot find the method with ZZLat as argument. Did you upload your changes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I did not know that you can see when I mark a conversation as resolved. I thought that I can use this for having a better overall view of the feedback. I am sorry, I can imagine that this led to confusion. The code should be uploaded now.


Return the roots `r` of a given hyperbolic reflection lattice represented by its corresponding Gram matrix `Q` with squared length contained in `root_lengths` and
by increasing order of the value $\frac{r.v_0)^2}{r^2}, stopping by `upper_bound`.
If `root_lengths` is not defined it takes all possible values of $r^2$.
If `v0` lies on a root hyperplane and if there is no given `direction_vector` it is a random choice which reflection chamber
next to `v0` will be computed.

# Arguments
- 'Q': symmetric $\Z$ matrix -- the corresponding Gram matrix
- 'upper_bound': the upper bound of the value $\frac{(r.v_0)^2}{r^2}$
- 'v0': primitive row vector with $v^2 > 0$
- 'root_lengths': the possible integer values of $r^2$
- 'direction_vector': row vector `v1` with $v_0.v_1 = 0$ and $v.v_1 \neq 0$ for all possible roots `v` with $v.v_0 = 0$
"""
function vinberg_algorithm(Q::ZZMatrix, upper_bound::Rational; v0 = matrix(ZZ, 1, 1, [0])::ZZMatrix, root_lengths=ZZRingElem[]::Vector{ZZRingElem}, direction_vector = matrix(ZZ, 1, 1, [0])::ZZMatrix)
@req is_symmetric(Q) "Matrix is not symmetric"
v0 = _check_v0(Q, v0)
if isempty(root_lengths)
real_root_lengths = _all_root_lengths(Q)
else
real_root_lengths = _check_root_lengths(Q, root_lengths) # sort out impossible lengths
end
iteration = _distance_indices(upper_bound, real_root_lengths) # find the right order to iterate through v.v0 and v^2
roots = _distance_0(Q, v0, real_root_lengths, direction_vector) # special case v.v0 = 0
for (n, k) in iteration # we search for vectors which solve n = v.v0 and k = v^2
@vprintln :Vinberg 1 "computing roots of squared length v^2=$(k) and v.v0 = $(n)"
possible_Vec = Oscar.short_vectors_affine(Q, v0, QQ(n), k)
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
for v in possible_Vec
if v in roots
continue
end
v = _is_primitive(v)
if _crystallographic_condition(Q, v)
if _check_coorientation(Q, roots, v, v0)
ElMerkel marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +270 to +271
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the hot loop. So these two function will be called many times. Can you make them non-allocating?

push!(roots, v)
end
end
end
end
return roots
end
Loading
Loading