Skip to content

Commit

Permalink
Trac #33915: inseparable elliptic-curve isogenies
Browse files Browse the repository at this point in the history
We implement an `EllipticCurveHom` child class
`EllipticCurveHom_frobenius` encapsulating purely inseparable
''Frobenius isogenies''. As every isogeny decomposes into a separable
and a purely inseparable part, we can (together with
`EllipticCurveHom_composite`) now express any isogeny between two
elliptic curves in Sage.

One immediate application (also implemented in the patch) is that
separable isogenies of degree divisible by the characteristic now have a
working `.dual()` method. Other than that, changes to the existing
codebase are kept minimal.

This is one of the items on the "isogeny wish-list" #7368. It is also an
important step towards implementing endomorphism rings later; cf.
comment:3:ticket:32826.

Diff without the dependency: https://git.sagemath.org/sage.git/diff?id2=
79ae468&id=e953939d23995c0c26964dc969fa
69cea52ee1c4

URL: https://trac.sagemath.org/33915
Reported by: lorenz
Ticket author(s): Lorenz Panny, Mickaël Montessinos
Reviewer(s): John Cremona
  • Loading branch information
Release Manager committed Jan 2, 2023
2 parents 98b22eb + 2ebbfae commit 5905da7
Show file tree
Hide file tree
Showing 7 changed files with 784 additions and 42 deletions.
1 change: 1 addition & 0 deletions src/doc/en/reference/arithmetic_curves/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Maps between them
sage/schemes/elliptic_curves/hom_velusqrt
sage/schemes/elliptic_curves/hom_composite
sage/schemes/elliptic_curves/hom_scalar
sage/schemes/elliptic_curves/hom_frobenius
sage/schemes/elliptic_curves/isogeny_small_degree


Expand Down
116 changes: 82 additions & 34 deletions src/sage/schemes/elliptic_curves/ell_curve_isogeny.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
use of univariate vs. bivariate polynomials and rational functions.
- Lorenz Panny (2022-04): major cleanup of code and documentation
- Lorenz Panny (2022): inseparable duals
"""

# ****************************************************************************
Expand Down Expand Up @@ -2954,6 +2955,31 @@ def dual(self):
sage: (Xm, Ym) == E.multiplication_by_m(5)
True
Inseparable duals should be computed correctly::
sage: z2 = GF(71^2).gen()
sage: E = EllipticCurve(j=57*z2+51)
sage: E.isogeny(3*E.lift_x(0)).dual()
Composite morphism of degree 71 = 71*1^2:
From: Elliptic Curve defined by y^2 = x^3 + (32*z2+67)*x + (24*z2+37) over Finite Field in z2 of size 71^2
To: Elliptic Curve defined by y^2 = x^3 + (41*z2+56)*x + (18*z2+42) over Finite Field in z2 of size 71^2
sage: E.isogeny(E.lift_x(0)).dual()
Composite morphism of degree 213 = 71*3:
From: Elliptic Curve defined by y^2 = x^3 + (58*z2+31)*x + (34*z2+58) over Finite Field in z2 of size 71^2
To: Elliptic Curve defined by y^2 = x^3 + (41*z2+56)*x + (18*z2+42) over Finite Field in z2 of size 71^2
...even if pre- or post-isomorphisms are present::
sage: from sage.schemes.elliptic_curves.weierstrass_morphism import WeierstrassIsomorphism
sage: phi = E.isogeny(E.lift_x(0))
sage: pre = ~WeierstrassIsomorphism(phi.domain(), (z2,2,3,4))
sage: post = WeierstrassIsomorphism(phi.codomain(), (5,6,7,8))
sage: phi = post * phi * pre
sage: phi.dual()
Composite morphism of degree 213 = 71*3:
From: Elliptic Curve defined by y^2 + 17*x*y + 45*y = x^3 + 30*x^2 + (6*z2+64)*x + (48*z2+65) over Finite Field in z2 of size 71^2
To: Elliptic Curve defined by y^2 + (60*z2+22)*x*y + (69*z2+37)*y = x^3 + (32*z2+48)*x^2 + (19*z2+58)*x + (56*z2+22) over Finite Field in z2 of size 71^2
TESTS:
Test for :trac:`23928`::
Expand Down Expand Up @@ -3003,55 +3029,77 @@ def dual(self):
return self.__dual

# trac 7096
E1, E2pr, pre_isom, post_isom = compute_intermediate_curves(self.codomain(), self.domain())
E1, E2pr, _, _ = compute_intermediate_curves(self.codomain(), self.domain())

F = self.__base_field
d = self._degree

# trac 7096
if F(d) == 0:
raise NotImplementedError("the dual isogeny is not separable: only separable isogenies are currently implemented")

# trac 7096
# this should take care of the case when the isogeny is not normalized.
u = self.scaling_factor()
isom = WeierstrassIsomorphism(E2pr, (u/F(d), 0, 0, 0))

E2 = isom.codomain()
from sage.schemes.elliptic_curves.hom_composite import EllipticCurveHom_composite

pre_isom = self._codomain.isomorphism_to(E1)
post_isom = E2.isomorphism_to(self._domain)
if F(d) == 0: # inseparable dual!
p = F.characteristic()
k = d.valuation(p)

phi_hat = EllipticCurveIsogeny(E1, None, E2, d)
from sage.schemes.elliptic_curves.hom_frobenius import EllipticCurveHom_frobenius
frob = EllipticCurveHom_frobenius(self._codomain, k)

phi_hat._set_pre_isomorphism(pre_isom)
phi_hat._set_post_isomorphism(post_isom)
phi_hat.__perform_inheritance_housekeeping()
dsep = d // p**k
if dsep > 1:
#TODO: We could also use resultants here; this is much
# faster in some cases (but seems worse in general).
# Presumably there should be a wrapper function that
# decides on the fly which method to use.
# Eventually this should become a .separable_part() method.

assert phi_hat.codomain() == self.domain()
f = self.kernel_polynomial()

# trac 7096 : this adjusts a posteriori the automorphism on
# the codomain of the dual isogeny. we used _a_ Weierstrass
# isomorphism to get to the original curve, but we may have to
# change it by an automorphism. We impose the condition that
# the composition has the degree as a leading coefficient in
# the formal expansion.
psi = self._domain.division_polynomial(p)
mu_num = self._domain._multiple_x_numerator(p)
mu_den = self._domain._multiple_x_denominator(p)

phihat_sc = phi_hat.scaling_factor()
for _ in range(k):
f //= f.gcd(psi)
S = f.parent().quotient_ring(f)
mu = S(mu_num) / S(mu_den)
f = mu.minpoly()

sc = u * phihat_sc/F(d)
sep = self._domain.isogeny(f, codomain=frob.codomain()).dual()

assert sc != 0, "bug in dual()"
else:
sep = frob.codomain().isomorphism_to(self._domain)

if sc != 1:
auts = self._domain.automorphisms()
aut = [a for a in auts if a.u == sc]
assert len(aut) == 1, "bug in dual()"
phi_hat._set_post_isomorphism(aut[0])
phi_hat = EllipticCurveHom_composite.from_factors([frob, sep])

self.__dual = phi_hat
from sage.schemes.elliptic_curves.hom import find_post_isomorphism
mult = self._domain.scalar_multiplication(d)
rhs = phi_hat * self
corr = find_post_isomorphism(mult, rhs)
self.__dual = corr * phi_hat
return self.__dual

return phi_hat
else:
# trac 7096
# this should take care of the case when the isogeny is not normalized.
u = self.scaling_factor()
E2 = E2pr.change_weierstrass_model(u/F(d), 0, 0, 0)

phi_hat = EllipticCurveIsogeny(E1, None, E2, d)

pre_iso = self._codomain.isomorphism_to(E1)
post_iso = E2.isomorphism_to(self._domain)

# assert phi_hat.scaling_factor() == 1
sc = u * pre_iso.scaling_factor() * post_iso.scaling_factor() / F(d)
if not sc.is_one():
auts = self._codomain.automorphisms()
aut = [a for a in auts if a.u == sc]
assert len(aut) == 1, "bug in dual()"
pre_iso *= aut[0]

phi_hat._set_pre_isomorphism(pre_iso)
phi_hat._set_post_isomorphism(post_iso)
phi_hat.__perform_inheritance_housekeeping()
return phi_hat


@staticmethod
Expand Down
22 changes: 22 additions & 0 deletions src/sage/schemes/elliptic_curves/ell_finite_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,28 @@ def frobenius(self):
else:
return R.gen(1)

def frobenius_endomorphism(self):
r"""
Return the `q`-power Frobenius endomorphism of this elliptic
curve, where `q` is the cardinality of the (finite) base field.
EXAMPLES::
sage: F.<t> = GF(11^4)
sage: E = EllipticCurve([t,t])
sage: E.frobenius_endomorphism()
Frobenius endomorphism of degree 14641 = 11^4:
From: Elliptic Curve defined by y^2 = x^3 + t*x + t over Finite Field in t of size 11^4
To: Elliptic Curve defined by y^2 = x^3 + t*x + t over Finite Field in t of size 11^4
sage: E.frobenius_endomorphism() == E.frobenius_isogeny(4)
True
.. SEEALSO::
:meth:`~sage.schemes.elliptic_curves.ell_generic.EllipticCurve_generic.frobenius_isogeny`
"""
return self.frobenius_isogeny(self.base_field().degree())

def cardinality_pari(self):
r"""
Return the cardinality of ``self`` using PARI.
Expand Down
31 changes: 31 additions & 0 deletions src/sage/schemes/elliptic_curves/ell_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2404,6 +2404,37 @@ def scalar_multiplication(self, m):
from sage.schemes.elliptic_curves.hom_scalar import EllipticCurveHom_scalar
return EllipticCurveHom_scalar(self, m)

def frobenius_isogeny(self, n=1):
r"""
Return the `n`-power Frobenius isogeny from this curve to
its Galois conjugate.
The Frobenius *endo*\morphism is the special case where `n`
is divisible by the degree of the base ring of the curve.
.. SEEALSO::
:meth:`~sage.schemes.elliptic_curves.ell_finite_field.EllipticCurve_finite_field.frobenius_endomorphism`
EXAMPLES::
sage: z3, = GF(13^3).gens()
sage: E = EllipticCurve([z3,z3^2])
sage: E.frobenius_isogeny()
Frobenius isogeny of degree 13:
From: Elliptic Curve defined by y^2 = x^3 + z3*x + z3^2 over Finite Field in z3 of size 13^3
To: Elliptic Curve defined by y^2 = x^3 + (5*z3^2+7*z3+11)*x + (5*z3^2+12*z3+1) over Finite Field in z3 of size 13^3
sage: E.frobenius_isogeny(3)
Frobenius endomorphism of degree 2197 = 13^3:
From: Elliptic Curve defined by y^2 = x^3 + z3*x + z3^2 over Finite Field in z3 of size 13^3
To: Elliptic Curve defined by y^2 = x^3 + z3*x + z3^2 over Finite Field in z3 of size 13^3
"""
p = self.base_ring().characteristic()
if not p:
raise ValueError('Frobenius isogeny only exists in positive characteristic')
from sage.schemes.elliptic_curves.hom_frobenius import EllipticCurveHom_frobenius
return EllipticCurveHom_frobenius(self, n)

def isomorphism_to(self, other):
"""
Given another weierstrass model ``other`` of self, return an
Expand Down
110 changes: 105 additions & 5 deletions src/sage/schemes/elliptic_curves/hom.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- :class:`~sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism`
- :class:`~sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite`
- :class:`~sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar`
- :class:`~sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius`
- :class:`~sage.schemes.elliptic_curves.hom_velusqrt.EllipticCurveHom_velusqrt`
AUTHORS:
Expand Down Expand Up @@ -290,6 +291,7 @@ def kernel_polynomial(self):
- :meth:`sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism.kernel_polynomial`
- :meth:`sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite.kernel_polynomial`
- :meth:`sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar.kernel_polynomial`
- :meth:`sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius.kernel_polynomial`
TESTS::
Expand All @@ -311,6 +313,7 @@ def dual(self):
- :meth:`sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism.dual`
- :meth:`sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite.dual`
- :meth:`sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar.dual`
- :meth:`sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius.dual`
TESTS::
Expand All @@ -334,6 +337,7 @@ def rational_maps(self):
- :meth:`sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism.rational_maps`
- :meth:`sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite.rational_maps`
- :meth:`sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar.rational_maps`
- :meth:`sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius.rational_maps`
TESTS::
Expand All @@ -356,6 +360,7 @@ def x_rational_map(self):
- :meth:`sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism.x_rational_map`
- :meth:`sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite.x_rational_map`
- :meth:`sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar.x_rational_map`
- :meth:`sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius.x_rational_map`
TESTS::
Expand Down Expand Up @@ -436,15 +441,15 @@ def formal(self, prec=20):
Eh = self._domain.formal()
f, g = self.rational_maps()
xh = Eh.x(prec=prec)
assert xh.valuation() == -2, f"xh has valuation {xh.valuation()} (should be -2)"
assert not self.is_separable() or xh.valuation() == -2, f"xh has valuation {xh.valuation()} (should be -2)"
yh = Eh.y(prec=prec)
assert yh.valuation() == -3, f"yh has valuation {yh.valuation()} (should be -3)"
assert not self.is_separable() or yh.valuation() == -3, f"yh has valuation {yh.valuation()} (should be -3)"
fh = f(xh,yh)
assert fh.valuation() == -2, f"fh has valuation {fh.valuation()} (should be -2)"
assert not self.is_separable() or fh.valuation() == -2, f"fh has valuation {fh.valuation()} (should be -2)"
gh = g(xh,yh)
assert gh.valuation() == -3, f"gh has valuation {gh.valuation()} (should be -3)"
assert not self.is_separable() or gh.valuation() == -3, f"gh has valuation {gh.valuation()} (should be -3)"
th = -fh/gh
assert th.valuation() == +1, f"th has valuation {th.valuation()} (should be +1)"
assert not self.is_separable() or th.valuation() == +1, f"th has valuation {th.valuation()} (should be +1)"
return th

def is_normalized(self):
Expand Down Expand Up @@ -536,6 +541,7 @@ def is_separable(self):
- :meth:`sage.schemes.elliptic_curves.weierstrass_morphism.WeierstrassIsomorphism.is_separable`
- :meth:`sage.schemes.elliptic_curves.hom_composite.EllipticCurveHom_composite.is_separable`
- :meth:`sage.schemes.elliptic_curves.hom_scalar.EllipticCurveHom_scalar.is_separable`
- :meth:`sage.schemes.elliptic_curves.hom_frobenius.EllipticCurveHom_frobenius.is_separable`
TESTS::
Expand Down Expand Up @@ -781,3 +787,97 @@ def compare_via_evaluation(left, right):
assert False, "couldn't find a point of infinite order"
else:
raise NotImplementedError('not implemented for this base field')


def find_post_isomorphism(phi, psi):
r"""
Given two isogenies `\phi: E\to E'` and `\psi: E\to E''`
which are equal up to post-isomorphism defined over the
same field, find that isomorphism.
In other words, this function computes an isomorphism
`\alpha: E'\to E''` such that `\alpha\circ\phi = \psi`.
ALGORITHM:
Start with a list of all isomorphisms `E'\to E''`. Then
repeatedly evaluate `\phi` and `\psi` at random points
`P` to filter the list for isomorphisms `\alpha` with
`\alpha(\phi(P)) = \psi(P)`. Once only one candidate is
left, return it. Periodically extend the base field to
avoid getting stuck (say, if all candidate isomorphisms
act the same on all rational points).
EXAMPLES::
sage: from sage.schemes.elliptic_curves.hom import find_post_isomorphism
sage: E = EllipticCurve(GF(7^2), [1,0])
sage: f = E.scalar_multiplication(1)
sage: g = choice(E.automorphisms())
sage: find_post_isomorphism(f, g) == g
True
::
sage: from sage.schemes.elliptic_curves.weierstrass_morphism import WeierstrassIsomorphism
sage: from sage.schemes.elliptic_curves.hom_composite import EllipticCurveHom_composite
sage: F.<i> = GF(883^2, modulus=x^2+1)
sage: E = EllipticCurve(F, [1,0])
sage: P = E.lift_x(117)
sage: Q = E.lift_x(774)
sage: w = WeierstrassIsomorphism(E, [i,0,0,0])
sage: phi = EllipticCurveHom_composite(E, [P,w(Q)]) * w
sage: psi = EllipticCurveHom_composite(E, [Q,w(P)])
sage: phi.kernel_polynomial() == psi.kernel_polynomial()
True
sage: find_post_isomorphism(phi, psi)
Elliptic-curve morphism:
From: Elliptic Curve defined by y^2 = x^3 + 320*x + 482 over Finite Field in i of size 883^2
To: Elliptic Curve defined by y^2 = x^3 + 320*x + 401 over Finite Field in i of size 883^2
Via: (u,r,s,t) = (882*i, 0, 0, 0)
"""
E = phi.domain()
if psi.domain() != E:
raise ValueError('domains do not match')

isos = phi.codomain().isomorphisms(psi.codomain())
if not isos:
raise ValueError('codomains not isomorphic')

F = E.base_ring()
from sage.rings.finite_rings import finite_field_base
from sage.rings.number_field import number_field_base

if isinstance(F, finite_field_base.FiniteField):
while len(isos) > 1:
for _ in range(20):
P = E.random_point()
im_phi, im_psi = (phi._eval(P), psi._eval(P))
isos = [iso for iso in isos if iso._eval(im_phi) == im_psi]
if len(isos) <= 1:
break
else:
E = E.base_extend(E.base_field().extension(2))

elif isinstance(F, number_field_base.NumberField):
for _ in range(100):
P = E.lift_x(F.random_element(), extend=True)
if P.has_finite_order():
continue
break
else:
assert False, "couldn't find a point of infinite order"
im_phi, im_psi = (phi._eval(P), psi._eval(P))
isos = [iso for iso in isos if iso._eval(im_phi) == im_psi]

else:
# fall back to generic method
sc = psi.scaling_factor() / phi.scaling_factor()
isos = [iso for iso in isos if iso.u == sc]

assert len(isos) <= 1
if isos:
return isos[0]

# found no suitable isomorphism -- either doesn't exist or a bug
raise ValueError('isogenies not equal up to post-isomorphism')
Loading

0 comments on commit 5905da7

Please sign in to comment.