Skip to content

Commit

Permalink
gh-36805: fast path for Vélu isogenies with a single kernel generator
Browse files Browse the repository at this point in the history
    
The current implementation of Vélu's formulas always assumes that the
kernel subgroup is given by an arbitrary set of generators: The
important special case of a single generator is handled using the same
code as the general case.
In this patch we add a fast path to handle a single generator, which can
yield big speedups. Example:
```sage
sage: E = EllipticCurve(GF(2^127-1), [1,1,1,1,1])
sage: P = E.lift_x(73833238617088645877502541346296796686)
sage: P.set_order(2543)
sage: %timeit E.isogeny(P)
```

Sage 10.2:
```
63.6 ms ± 588 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

With this patch:
```
33.3 ms ± 98.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
    
URL: #36805
Reported by: Lorenz Panny
Reviewer(s): Giacomo Pope
  • Loading branch information
Release Manager committed Feb 11, 2024
2 parents 5a3f8eb + 81eb2c3 commit bbe70e7
Showing 1 changed file with 90 additions and 29 deletions.
119 changes: 90 additions & 29 deletions src/sage/schemes/elliptic_curves/ell_curve_isogeny.py
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ def __init__(self, E, kernel, codomain=None, degree=None, model=None, check=True
self.__algorithm = algorithm

if algorithm == 'velu':
self.__init_from_kernel_list(kernel)
self.__init_from_kernel_gens(kernel)
elif algorithm == 'kohel':
self.__init_from_kernel_polynomial(kernel)
else:
Expand Down Expand Up @@ -1870,7 +1870,7 @@ def __setup_post_isomorphism(self, codomain, model):
# Setup function for Velu's formula
#

def __init_from_kernel_list(self, kernel_gens):
def __init_from_kernel_gens(self, kernel_gens):
r"""
Private function that initializes the isogeny from a list of
points which generate the kernel (For Vélu's formulas.)
Expand All @@ -1884,7 +1884,7 @@ def __init_from_kernel_list(self, kernel_gens):
Isogeny of degree 2
from Elliptic Curve defined by y^2 = x^3 + 6*x over Finite Field of size 7
to Elliptic Curve defined by y^2 = x^3 + 4*x over Finite Field of size 7
sage: phi._EllipticCurveIsogeny__init_from_kernel_list([E(0), E((0,0))])
sage: phi._EllipticCurveIsogeny__init_from_kernel_gens([E(0), E((0,0))])
The following example demonstrates the necessity of avoiding any calls
to P.order(), since such calls involve factoring the group order which
Expand All @@ -1907,6 +1907,14 @@ def __init_from_kernel_list(self, kernel_gens):
if not P.has_finite_order():
raise ValueError("given kernel contains point of infinite order")

self.__kernel_mod_sign = {}
self.__v = self.__w = 0

# Fast path: The kernel is given by a single generating point.
if len(kernel_gens) == 1 and kernel_gens[0]:
self.__init_from_kernel_point(kernel_gens[0])
return

# Compute a list of points in the subgroup generated by the
# points in kernel_gens. This is very naive: when finite
# subgroups are implemented better, this could be simplified,
Expand All @@ -1927,12 +1935,86 @@ def all_multiples(itr, terminal):

self._degree = Integer(len(kernel_set))
self.__kernel_list = list(kernel_set)
self.__sort_kernel_list()
self.__init_from_kernel_list()

#
# Precompute the values in Velu's Formula.
#
def __sort_kernel_list(self):
def __update_kernel_data(self, xQ, yQ):
r"""
Internal helper function to update some data coming from the
kernel points of this isogeny when using Vélu's formulas.
TESTS:
The following example inherently exercises this function::
sage: E = EllipticCurve(GF(7), [0,0,0,-1,0])
sage: P = E((4,2))
sage: phi = EllipticCurveIsogeny(E, [P,P]); phi # implicit doctest
Isogeny of degree 4
from Elliptic Curve defined by y^2 = x^3 + 6*x over Finite Field of size 7
to Elliptic Curve defined by y^2 = x^3 + 2*x over Finite Field of size 7
"""
a1, a2, a3, a4, _ = self._domain.a_invariants()

gxQ = (3*xQ + 2*a2)*xQ + a4 - a1*yQ
gyQ = -2*yQ - a1*xQ - a3

uQ = gyQ**2

if 2*yQ == -a1*xQ - a3: # Q is 2-torsion
vQ = gxQ
else: # Q is not 2-torsion
vQ = 2*gxQ - a1*gyQ

self.__kernel_mod_sign[xQ] = yQ, gxQ, gyQ, vQ, uQ

self.__v += vQ
self.__w += uQ + xQ*vQ

def __init_from_kernel_point(self, ker):
r"""
Private function with functionality equivalent to
:meth:`__init_from_kernel_list`, but optimized for when
the kernel is given by a single point.
TESTS:
The following example inherently exercises this function::
sage: E = EllipticCurve(GF(7), [0,0,0,-1,0])
sage: P = E((4,2))
sage: phi = EllipticCurveIsogeny(E, P); phi # implicit doctest
Isogeny of degree 4
from Elliptic Curve defined by y^2 = x^3 + 6*x over Finite Field of size 7
to Elliptic Curve defined by y^2 = x^3 + 2*x over Finite Field of size 7
We check that the result is the same as for :meth:`__init_from_kernel_list`::
sage: psi = EllipticCurveIsogeny(E, [P, P]); psi
Isogeny of degree 4
from Elliptic Curve defined by y^2 = x^3 + 6*x over Finite Field of size 7
to Elliptic Curve defined by y^2 = x^3 + 2*x over Finite Field of size 7
sage: phi == psi
True
"""
self._degree = Integer(1)

Q, prevQ = ker, self._domain(0)

while Q and Q != -prevQ:
self.__update_kernel_data(*Q.xy())

if Q == -Q:
self._degree += 1
break

prevQ = Q
Q += ker
self._degree += 2

def __init_from_kernel_list(self):
r"""
Private function that sorts the list of points in the kernel
(For Vélu's formulas). Sorts out the 2-torsion points, and
Expand All @@ -1944,43 +2026,22 @@ def __sort_kernel_list(self):
sage: E = EllipticCurve(GF(7), [0,0,0,-1,0])
sage: P = E((4,2))
sage: phi = EllipticCurveIsogeny(E, P); phi
sage: phi = EllipticCurveIsogeny(E, [P,P]); phi # implicit doctest
Isogeny of degree 4
from Elliptic Curve defined by y^2 = x^3 + 6*x over Finite Field of size 7
to Elliptic Curve defined by y^2 = x^3 + 2*x over Finite Field of size 7
sage: phi._EllipticCurveIsogeny__sort_kernel_list()
"""
a1, a2, a3, a4, _ = self._domain.a_invariants()

self.__kernel_mod_sign = {}
v = w = 0

for Q in self.__kernel_list:

if Q.is_zero():
continue

xQ,yQ = Q.xy()
xQ, yQ = Q.xy()

if xQ in self.__kernel_mod_sign:
continue

gxQ = (3*xQ + 2*a2)*xQ + a4 - a1*yQ
gyQ = -2*yQ - a1*xQ - a3

uQ = gyQ**2

if 2*yQ == -a1*xQ - a3: # Q is 2-torsion
vQ = gxQ
else: # Q is not 2-torsion
vQ = 2*gxQ - a1*gyQ

self.__kernel_mod_sign[xQ] = yQ, gxQ, gyQ, vQ, uQ

v += vQ
w += uQ + xQ*vQ

self.__v, self.__w = v, w
self.__update_kernel_data(xQ, yQ)

#
# Velu's formula computing the codomain curve
Expand Down

0 comments on commit bbe70e7

Please sign in to comment.