Skip to content

Commit 67e6234

Browse files
author
Release Manager
committed
gh-38259: Fixed and improvements in `is_LLL_reduced` and `approximate_closest_vector` This fixes #38115. Along with the goal stated there I have fixed some stuff I wasn't happy with from the first iteration of `approximate_closest_vector`. These changes could be breaking but seeing as there has been no release since its introduction this shouldn't be a problem. Changes: - Added the `algorithm` parameter to `is_LLL_reduced` along with the new algorithm `fpLLL` which uses fpylll's `LLL.is_reduced` method, **and made it the default**, as I see no advantages with the old implementation. - Added the `algorithm` parameter to `approximate_closest_vector` along with the new algorithms `embedding` and `rounding_off`, **and made `embedding` the default.** - ~Changed the alias of `approximate_closest_vector` from `babai` to `cvp` to be more accurate but still short.~ I'm getting weird errors when building documentation locally but I don't think it's related to my changes, so I haven't been able to check if the doc build looks alright yet. (and it won't build on my own repo b/c my GitHub username has uppercase letters in it :) ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [ ] I have updated the documentation and checked the documentation preview. URL: #38259 Reported by: TheBlupper Reviewer(s): Giacomo Pope, Matthias Köppe, TheBlupper
2 parents 61e1f4a + 605c02a commit 67e6234

File tree

2 files changed

+93
-35
lines changed

2 files changed

+93
-35
lines changed

src/sage/matrix/matrix_integer_dense.pyx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3297,7 +3297,7 @@ cdef class Matrix_integer_dense(Matrix_dense):
32973297
else:
32983298
return R
32993299

3300-
def is_LLL_reduced(self, delta=None, eta=None):
3300+
def is_LLL_reduced(self, delta=None, eta=None, algorithm='fpLLL'):
33013301
r"""
33023302
Return ``True`` if this lattice is `(\delta, \eta)`-LLL reduced.
33033303
See ``self.LLL`` for a definition of LLL reduction.
@@ -3308,6 +3308,8 @@ cdef class Matrix_integer_dense(Matrix_dense):
33083308
33093309
- ``eta`` -- (default: `0.501`) parameter `\eta` as described above
33103310
3311+
- ``algorithm`` -- either ``'fpLLL'`` (default) or ``'sage'``
3312+
33113313
EXAMPLES::
33123314
33133315
sage: A = random_matrix(ZZ, 10, 10)
@@ -3316,6 +3318,15 @@ cdef class Matrix_integer_dense(Matrix_dense):
33163318
False
33173319
sage: L.is_LLL_reduced()
33183320
True
3321+
3322+
The ``'sage'`` algorithm currently does not work for matrices with
3323+
linearly dependent rows::
3324+
3325+
sage: A = matrix(ZZ, [[1, 2, 3], [2, 4, 6]])
3326+
sage: A.is_LLL_reduced(algorithm='sage')
3327+
Traceback (most recent call last):
3328+
...
3329+
ValueError: linearly dependent input for module version of Gram-Schmidt
33193330
"""
33203331
if eta is None:
33213332
eta = 0.501
@@ -3330,20 +3341,27 @@ cdef class Matrix_integer_dense(Matrix_dense):
33303341
if eta < 0.5:
33313342
raise TypeError("eta must be >= 0.5")
33323343

3333-
# this is pretty slow
3334-
import sage.modules.misc
3335-
G, mu = sage.modules.misc.gram_schmidt(self.rows())
3336-
#For any $i>j$, we have $|mu_{i, j}| <= \eta$
3337-
for e in mu.list():
3338-
if e.abs() > eta:
3339-
return False
3340-
3341-
#For any $i<d$, we have $\delta |b_i^*|^2 <= |b_{i+1}^* + mu_{i+1, i} b_i^* |^2$
3342-
norms = [G[i].norm()**2 for i in range(len(G))]
3343-
for i in range(1,self.nrows()):
3344-
if norms[i] < (delta - mu[i,i-1]**2) * norms[i-1]:
3345-
return False
3346-
return True
3344+
if algorithm == 'fpLLL':
3345+
from fpylll import LLL, IntegerMatrix
3346+
A = IntegerMatrix.from_matrix(self)
3347+
return LLL.is_reduced(A, delta=delta, eta=eta)
3348+
elif algorithm == 'sage':
3349+
# This is pretty slow
3350+
import sage.modules.misc
3351+
G, mu = sage.modules.misc.gram_schmidt(self.rows())
3352+
# For any $i>j$, we have $|mu_{i, j}| <= \eta$
3353+
for e in mu.list():
3354+
if e.abs() > eta:
3355+
return False
3356+
3357+
# For any $i<d$, we have $\delta |b_i^*|^2 <= |b_{i+1}^* + mu_{i+1, i} b_i^* |^2$
3358+
norms = [G[i].norm()**2 for i in range(len(G))]
3359+
for i in range(1,self.nrows()):
3360+
if norms[i] < (delta - mu[i,i-1]**2) * norms[i-1]:
3361+
return False
3362+
return True
3363+
else:
3364+
raise ValueError("algorithm must be one of 'fpLLL' or 'sage'")
33473365

33483366
def prod_of_row_sums(self, cols):
33493367
"""

src/sage/modules/free_module_integer.py

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
##############################################################################
3333

3434
from sage.rings.integer_ring import ZZ
35+
from sage.rings.rational_field import QQ
3536
from sage.matrix.constructor import matrix
3637
from sage.misc.cachefunc import cached_method
3738
from sage.modules.free_module import FreeModule_submodule_with_basis_pid, FreeModule_ambient_pid
@@ -784,16 +785,11 @@ def CVPP_2V(t, V, voronoi_cell):
784785
i -= 1
785786
return t - t_new
786787

787-
def approximate_closest_vector(self, t, delta=None, *args, **kwargs):
788+
def approximate_closest_vector(self, t, delta=None, algorithm='embedding', *args, **kwargs):
788789
r"""
789-
Compute a vector `w` such that
790-
791-
.. MATH::
792-
793-
|t-w|<(\frac{1}{\delta-\frac{1}{4}})^{d/2}|t-u|
794-
795-
where `u` is the closest lattice point to `t`, `\delta` is the LLL
796-
reduction parameter, and `d` is the dimension of the lattice.
790+
Compute a vector `w` in this lattice which is close to the target vector `t`.
791+
The ratio `\frac{|t-w|}{|t-u|}`, where `u` is the closest lattice vector to `t`,
792+
is exponential in the dimension of the lattice.
797793
798794
This will check whether the basis is already `\delta`-LLL-reduced
799795
and otherwise it will run LLL to make sure that it is. For more
@@ -805,6 +801,18 @@ def approximate_closest_vector(self, t, delta=None, *args, **kwargs):
805801
806802
- ``delta`` -- (default: ``0.99``) the LLL reduction parameter
807803
804+
- ``algorithm`` -- string (default: 'embedding'):
805+
806+
- ``'embedding'`` -- embeds the lattice in a d+1 dimensional space
807+
and seeks short vectors using LLL. This calls LLL twice but is
808+
usually still quick.
809+
810+
- ``'nearest_plane'`` -- uses the "NEAREST PLANE" algorithm from [Bab86]_
811+
812+
- ``'rounding_off'`` -- uses the "ROUNDING OFF" algorithm from [Bab86]_.
813+
This yields slightly worse results than the other algorithms but is
814+
at least faster than ``'nearest_plane'``.
815+
808816
- ``*args`` -- passed through to :meth:`LLL`
809817
810818
- ``**kwds`` -- passed through to :meth:`LLL`
@@ -825,30 +833,62 @@ def approximate_closest_vector(self, t, delta=None, *args, **kwargs):
825833
....: [0, 0, 101, 0], [-28, 39, 45, 1]], lll_reduce=False)
826834
sage: t = vector([1337]*4)
827835
sage: L.approximate_closest_vector(t, delta=0.26)
828-
(1348, 1340, 1383, 1337)
836+
(1331, 1324, 1349, 1334)
829837
sage: L.approximate_closest_vector(t, delta=0.99)
830838
(1326, 1349, 1339, 1345)
831839
sage: L.closest_vector(t)
832840
(1326, 1349, 1339, 1345)
833841
834-
ALGORITHM:
835-
836-
Uses the algorithm from [Bab86]_.
842+
sage: # Checking that the other algorithms work
843+
sage: L.approximate_closest_vector(t, algorithm='nearest_plane')
844+
(1326, 1349, 1339, 1345)
845+
sage: L.approximate_closest_vector(t, algorithm='rounding_off')
846+
(1331, 1324, 1349, 1334)
837847
"""
838848
if delta is None:
839849
delta = ZZ(99)/ZZ(100)
840850

841-
# bound checks on delta are performed in is_LLL_reduced
851+
# Bound checks on delta are performed in is_LLL_reduced
842852
if not self._reduced_basis.is_LLL_reduced(delta=delta):
843853
self.LLL(*args, delta=delta, **kwargs)
844854

845855
B = self._reduced_basis
846-
G = B.gram_schmidt()[0]
847856
t = vector(t)
848857

849-
b = t
850-
for i in reversed(range(G.nrows())):
851-
b -= B[i] * ((b * G[i]) / (G[i] * G[i])).round("even")
852-
return (t - b).change_ring(ZZ)
858+
if algorithm == 'embedding':
859+
L = matrix(QQ, B.nrows()+1, B.ncols()+1)
860+
L.set_block(0, 0, B)
861+
L.set_block(B.nrows(), 0, matrix(t))
862+
weight = (B[-1]*B[-1]).isqrt()+1 # Norm of the largest vector
863+
L[-1, -1] = weight
864+
865+
# The vector should be the last row but we iterate just in case
866+
for v in reversed(L.LLL(delta=delta, *args, **kwargs).rows()):
867+
if abs(v[-1]) == weight:
868+
return t - v[:-1]*v[-1].sign()
869+
raise ValueError('No suitable vector found in basis.'
870+
'This is a bug, please report it.')
871+
872+
elif algorithm == 'nearest_plane':
873+
G = B.gram_schmidt()[0]
874+
875+
b = t
876+
for i in reversed(range(G.nrows())):
877+
b -= B[i] * ((b * G[i]) / (G[i] * G[i])).round("even")
878+
return (t - b).change_ring(ZZ)
879+
880+
elif algorithm == 'rounding_off':
881+
# t = x*B might not have a solution over QQ so we instead solve
882+
# the system x*B*B^T = t*B^T which will be the "closest" solution
883+
# if it does not exist, same effect as using the psuedo-inverse
884+
sol = (B*B.T).solve_left(t*B.T)
885+
return vector(ZZ, [QQ(x).round('even') for x in sol])*B
853886

854-
babai = approximate_closest_vector
887+
else:
888+
raise ValueError("algorithm must be one of 'embedding', 'nearest_plane' or 'rounding_off'")
889+
890+
def babai(self, *args, **kwargs):
891+
"""
892+
Alias for :meth:`approximate_closest_vector`.
893+
"""
894+
return self.approximate_closest_vector(*args, **kwargs)

0 commit comments

Comments
 (0)