Skip to content

Commit fb7ef07

Browse files
author
Release Manager
committed
gh-36190: establish interface for instantiated classical modular polynomials Generic modular polynomials (i.e., as elements of $\mathbb Z[X,Y]$) grow very big very quickly. However, most algorithmic applications only require evaluations $\Phi_\ell(j,Y)$ of those polynomials. There are algorithms to compute such evaluations faster than computing-then- evaluating the full modular polynomial, for example due to [Sutherland](https://arxiv.org/abs/1202.3985). PARI implements both kinds of algorithms in `polmodular()`. This patch adds a common interface for (1) accessing Kohel's database of modular polynomials if available, (2) calling PARI's `polmodular()` to compute generic modular polynomials, and (3) calling PARI's `polmodular()` to compute instantiated modular polynomials. Results are opportunistically cached and reused whenever this seems to make sense. URL: #36190 Reported by: Lorenz Panny Reviewer(s): Lorenz Panny, Travis Scrimshaw
2 parents 839327a + f33ad68 commit fb7ef07

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

src/doc/en/reference/arithmetic_curves/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Maps between them
2525
sage/schemes/elliptic_curves/hom_scalar
2626
sage/schemes/elliptic_curves/hom_frobenius
2727
sage/schemes/elliptic_curves/isogeny_small_degree
28+
sage/schemes/elliptic_curves/mod_poly
2829

2930

3031
Elliptic curves over number fields

src/sage/schemes/elliptic_curves/all.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@
4040

4141
from .ell_curve_isogeny import EllipticCurveIsogeny, isogeny_codomain_from_kernel
4242

43+
lazy_import('sage.schemes.elliptic_curves.mod_poly', 'classical_modular_polynomial')
44+
4345
from .heegner import heegner_points, heegner_point
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
r"""
2+
Modular polynomials for elliptic curves
3+
4+
For a positive integer `\ell`, the classical modular polynomial
5+
`\Phi_\ell \in \ZZ[X,Y]` is characterized by the property that its
6+
zero set is exactly the set of pairs of `j`-invariants connected
7+
by a cyclic `\ell`-isogeny.
8+
9+
AUTHORS:
10+
11+
- Lorenz Panny (2023)
12+
"""
13+
14+
from sage.structure.element import parent
15+
16+
from sage.rings.integer_ring import ZZ
17+
18+
from sage.libs.pari import pari
19+
from cypari2.handle_error import PariError
20+
21+
from sage.databases.db_modular_polynomials import ClassicalModularPolynomialDatabase
22+
_db = ClassicalModularPolynomialDatabase()
23+
24+
_cache_bound = 100
25+
_cache = dict()
26+
27+
def classical_modular_polynomial(l, j=None):
28+
r"""
29+
Return the classical modular polynomial `\Phi_\ell`, either as a
30+
"generic" bivariate polynomial over `\ZZ`, or as an "instantiated"
31+
modular polynomial where one variable has been replaced by the
32+
given `j`-invariant.
33+
34+
Generic polynomials are cached up to a certain size of `\ell`,
35+
which significantly accelerates subsequent invocations with the
36+
same `\ell`. The default bound is `\ell \leq 100`, which can be
37+
adjusted using ``classical_modular_polynomial.set_cache_bound()``
38+
with a different value. Beware that modular polynomials are very
39+
big objects and the amount of memory consumed by the cache will
40+
grow rapidly when the bound is set to a large value.
41+
42+
INPUT:
43+
44+
- ``l`` -- positive integer.
45+
- ``j`` -- either ``None`` or a ring element:
46+
47+
* if ``None`` is given, the original modular polynomial
48+
is returned as an element of `\ZZ[X,Y]`
49+
* if a ring element `j \in R` is given, the evaluation
50+
`\Phi_\ell(j,Y)` is returned as an element of the
51+
univariate polynomial ring `R[Y]`
52+
53+
ALGORITHMS:
54+
55+
- The Kohel database
56+
:class:`~sage.databases.db_modular_polynomials.ClassicalModularPolynomialDatabase`
57+
- :pari:`polmodular`
58+
59+
EXAMPLES::
60+
61+
sage: classical_modular_polynomial(2)
62+
-X^2*Y^2 + X^3 + 1488*X^2*Y + 1488*X*Y^2 + Y^3 - 162000*X^2 + 40773375*X*Y - 162000*Y^2 + 8748000000*X + 8748000000*Y - 157464000000000
63+
sage: j = Mod(1728, 419)
64+
sage: classical_modular_polynomial(3, j)
65+
Y^4 + 230*Y^3 + 84*Y^2 + 118*Y + 329
66+
67+
Increasing the cache size can be useful for repeated invocations::
68+
69+
sage: %timeit classical_modular_polynomial(101) # not tested
70+
6.11 s ± 1.21 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
71+
sage: %timeit classical_modular_polynomial(101, GF(65537).random_element()) # not tested
72+
5.43 s ± 2.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
73+
74+
sage: classical_modular_polynomial.set_cache_bound(150) # not tested
75+
sage: %timeit classical_modular_polynomial(101) # not tested
76+
The slowest run took 10.35 times longer than the fastest. This could mean that an intermediate result is being cached.
77+
1.84 µs ± 1.84 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
78+
sage: %timeit classical_modular_polynomial(101, GF(65537).random_element()) # not tested
79+
59.8 ms ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
80+
81+
TESTS::
82+
83+
sage: q = random_prime(50)^randrange(1,4)
84+
sage: j = GF(q).random_element()
85+
sage: l = random_prime(50)
86+
sage: Y = polygen(parent(j), 'Y')
87+
sage: classical_modular_polynomial(l,j) == classical_modular_polynomial(l)(j,Y)
88+
True
89+
"""
90+
l = ZZ(l)
91+
92+
if j is None:
93+
# We are supposed to return the generic modular polynomial. First
94+
# check if it is already in the cache, then check the database,
95+
# finally compute it using PARI.
96+
try:
97+
return _cache[l]
98+
except KeyError:
99+
pass
100+
101+
try:
102+
Phi = ZZ['X,Y'](_db[l])
103+
except ValueError:
104+
try:
105+
pari_Phi = pari.polmodular(l)
106+
except PariError:
107+
raise NotImplementedError('modular polynomial is not in database and computing it on the fly is not yet implemented')
108+
d = {(i, j): c for i,f in enumerate(pari_Phi) for j, c in enumerate(f)}
109+
Phi = ZZ['X,Y'](d)
110+
111+
if l <= _cache_bound:
112+
_cache[l] = Phi
113+
114+
return Phi
115+
116+
R = parent(j)['Y']
117+
Y = R.gen()
118+
119+
# If the generic polynomial is in the cache or the database, evaluating
120+
# it directly should always be faster than recomputing it from scratch.
121+
if l in _cache:
122+
return _cache[l](j, Y)
123+
try:
124+
Phi = _db[l]
125+
except ValueError:
126+
pass
127+
else:
128+
if l <= _cache_bound:
129+
_cache[l] = ZZ['X,Y'](Phi)
130+
return Phi(j, Y)
131+
132+
# Now try to get the instantiated modular polynomial directly from PARI.
133+
# This should be slightly more efficient (in particular regarding memory
134+
# usage) than computing and evaluating the generic modular polynomial.
135+
try:
136+
pari_Phi = pari.polmodular(l, 0, j)
137+
except PariError:
138+
pass
139+
else:
140+
return R(pari_Phi)
141+
142+
# Nothing worked. Fall back to computing the generic modular polynomial
143+
# and simply evaluating it.
144+
return classical_modular_polynomial(l)(j, Y)
145+
146+
def _set_cache_bound(bnd):
147+
r"""
148+
Internal helper function to allow setting the caching cutoff for
149+
:func:`classical_modular_polynomial`.
150+
151+
Exposed as ``classical_modular_polynomial.set_cache_bound()``.
152+
153+
EXAMPLES::
154+
155+
sage: import sage.schemes.elliptic_curves.mod_poly as m
156+
sage: m._cache_bound
157+
100
158+
sage: m._set_cache_bound(123)
159+
sage: m._cache_bound
160+
123
161+
sage: classical_modular_polynomial.set_cache_bound is m._set_cache_bound
162+
True
163+
"""
164+
global _cache_bound
165+
_cache_bound = bnd
166+
167+
classical_modular_polynomial.set_cache_bound = _set_cache_bound

0 commit comments

Comments
 (0)