Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions lmfdb/ecnf/WebEllipticCurve.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,14 @@ def make_E(self):
self.fact_mindisc = latex_factorization(badprimes, mindisc_ords)
self.fact_mindisc_norm = latex_factorization(badnorms, mindisc_ords, sign=signDnorm)

# Szpiro ratio log(Norm(mindisc))/log(Norm(cond)). The
# attribute is set from the database row when the
# szpiro_ratio column exists; it is None for curves with
# everywhere good reduction, for which the ratio is not
# defined. The default here keeps this class working until
# the column has been added to ec_nfcurves.
self.szpiro_ratio = getattr(self, "szpiro_ratio", None)

j = self.field.parse_NFelt(self.jinv)
self.j = web_latex(j)
self.fact_j = None
Expand Down
22 changes: 21 additions & 1 deletion lmfdb/ecnf/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ def parse_cm_list(inp, query, qfield):

Ra = PolynomialRing(QQ,'a')

# The szpiro_ratio column is computed and uploaded by scripts/ecnf/generate_szpiro_ratio.py.
# All uses are guarded by this flag, which is a startup-time compatibility guard: it is
# evaluated once when this module is imported, so old-schema deployments keep working, but
# the web workers must be restarted after the column has been added and populated so that
# ecnf_columns, the search array and the sort choices below (also built at import time) are
# rebuilt. Refreshing the schema of a running worker is not enough.
HAVE_SZPIRO_RATIO = "szpiro_ratio" in db.ec_nfcurves.search_cols

ecnf_columns = SearchColumns([
MultiProcessedCol("label", "ec.curve_label", "Label", ["short_label", "field_label", "conductor_label", "iso_label", "number"],
lambda label, field, conductor, iso, number: '<a href="%s">%s</a>' % (
Expand All @@ -395,6 +403,7 @@ def parse_cm_list(inp, query, qfield):
ProcessedCol("conductor_norm", "ec.conductor", "Conductor norm", lambda v: web_latex_factored_integer(ZZ(v)), align="center"),
ProcessedCol("normdisc", "ec.discriminant", "Discriminant norm", lambda v: web_latex_factored_integer(ZZ(v)), align="center", default=False),
FloatCol("root_analytic_conductor", "lfunction.root_analytic_conductor", "Root analytic conductor", prec=5, default=False),
*([FloatCol("szpiro_ratio", "ec.szpiro_ratio", "Szpiro ratio", prec=5, default=False)] if HAVE_SZPIRO_RATIO else []),
ProcessedCol("bad_primes", "ec.bad_reduction", "Bad primes",
lambda primes: ", ".join(''.join(str(p.replace('w', 'a')).split('*')) for p in primes) if primes else r"\textsf{none}",
default=lambda info: info.get("bad_primes"), mathmode=True, align="center"),
Expand Down Expand Up @@ -512,6 +521,8 @@ def elliptic_curve_search(info, query):
parse_ints(info,query,'class_deg','class_deg')
parse_ints(info,query,'sha','analytic order of &#1064;')
parse_floats(info,query,'regulator',name='regulator',qfield='reg')
if HAVE_SZPIRO_RATIO:
parse_floats(info,query,'szpiro_ratio','Szpiro ratio')
parse_nf_jinv(info,query,'jinv','j-invariant',field_label=query.get('field_label'))

if info.get('one') == "yes":
Expand Down Expand Up @@ -795,7 +806,8 @@ class ECNFSearchArray(SearchArray):
("reg", "regulator", ["reg", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number']),
("sha", "analytic &#1064;", ["sha", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number']),
("class_size", "isogeny class size", ["class_size", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number']),
("class_deg", "isogeny class degree", ["class_deg", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number'])]
("class_deg", "isogeny class degree", ["class_deg", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number'])] \
+ ([("szpiro_ratio", "Szpiro ratio", ["szpiro_ratio", 'degree', 'signature', 'abs_disc', 'field_label', 'conductor_norm', 'conductor_label', 'iso_nlabel', 'number'])] if HAVE_SZPIRO_RATIO else [])
jump_example = "2.2.5.1-31.1-a1"
jump_egspan = "e.g. 2.2.5.1-31.1-a1 or 2.2.5.1-31.1-a"
jump_knowl = "ec.search_input"
Expand Down Expand Up @@ -947,6 +959,12 @@ def __init__(self):
label="Base change of",
knowl="ec.base_change",
example="11a.1")
szpiro_ratio = TextBox(
name="szpiro_ratio",
label="Szpiro ratio",
knowl="ec.szpiro_ratio",
example="4.5",
example_span="4.5 or 4-5")
count = CountBox()

self.browse_array = [
Expand All @@ -960,6 +978,7 @@ def __init__(self):
[class_size, class_deg],
[galois_image, nonmax_primes],
[base_change_label, reduction],
*([[szpiro_ratio]] if HAVE_SZPIRO_RATIO else []),
[jinv],
[count]
]
Expand All @@ -969,5 +988,6 @@ def __init__(self):
[deg_sig, bad_primes, Qcurves, torsion_structure, include_cm],
[sha, isodeg, class_size, reduction, galois_image],
[base_change_label, regulator, one, class_deg, nonmax_primes],
*([[szpiro_ratio]] if HAVE_SZPIRO_RATIO else []),
[jinv],
]
9 changes: 9 additions & 0 deletions lmfdb/ecnf/templates/ecnf-curve.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ <h2>{{ KNOWL('ec.invariants', title='Invariants')}}</h2>
</td>
</tr>

{% if ec.szpiro_ratio %}
<tr>
<td>{{ KNOWL('ec.szpiro_ratio', title='Szpiro ratio') }}:</td>
<td>$\sigma$</td>
<td>&approx;</td>
<td colspan="3">${{ ec.szpiro_ratio }}$</td>
</tr>
{% endif %}

</table>


Expand Down
114 changes: 114 additions & 0 deletions lmfdb/ecnf/test_ecnf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import os
import re

from lmfdb.tests import LmfdbTest


def szpiro_generator():
"""The scripts/ecnf/generate_szpiro_ratio.py module, loaded by path.

``scripts`` is not part of the lmfdb package, so it cannot simply be
imported; ``None`` is returned when it is absent (e.g. when only the
package has been installed).
"""
import importlib.util
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir,
"scripts", "ecnf", "generate_szpiro_ratio.py")
if not os.path.exists(path):
return None
spec = importlib.util.spec_from_file_location("generate_szpiro_ratio", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


class EllCurveTest(LmfdbTest):

# All tests should pass
Expand Down Expand Up @@ -121,6 +143,98 @@ def test_isodeg(self):
L = self.tc.get('/EllipticCurve/?start=0&torsion=1&isodeg=2')
assert 'No matches' in L.get_data(as_text=True)

def test_szpiro_ratio(self):
r"""
Test that the Szpiro ratio is displayed, searchable and sortable
once ec_nfcurves has the szpiro_ratio column, and that the pages
still work (without offering it) while the column is missing
"""
from lmfdb.ecnf.main import HAVE_SZPIRO_RATIO
# 2.2.5.1-31.1-a1 has Szpiro ratio exactly 1 (Norm(D_min) = Norm(N) = 31);
# 3.3.1369.1-1.1-a1 has everywhere good reduction, so no ratio at all.
curve_url = '/EllipticCurve/2.2.5.1/31.1/a/1'
egr_url = '/EllipticCurve/3.3.1369.1/1.1/a/1'
t = self.tc.get(curve_url).get_data(as_text=True)
assert 'Conductor norm' in t

if not HAVE_SZPIRO_RATIO:
# Compatibility branch: the pages load and offer no ratio anywhere.
# A szpiro_ratio constraint is deliberately ignored rather than
# raising, so the search below only shows that the page still works;
# it says nothing about filtering, which needs the column.
assert 'Szpiro ratio' not in t
assert 'Szpiro ratio' not in self.tc.get('/EllipticCurve/').get_data(as_text=True)
assert self.tc.get('/EllipticCurve/?field=2.2.5.1&szpiro_ratio=0.5-1.5').status_code == 200
return

# Displayed on the curve page, with the right value. Match the label
# text rather than the knowl markup around it: KNOWL() renders a plain
# label until ec.szpiro_ratio has been created, and an anchor after.
row = re.search(r'Szpiro ratio.*?</tr>', t, re.DOTALL)
assert row is not None, 'no Szpiro ratio row on %s' % curve_url
assert re.search(r'\$\s*1\.0\s*\$', row.group(0)), row.group(0)
# Omitted for a curve with everywhere good reduction, where it is undefined.
assert 'Szpiro ratio' not in self.tc.get(egr_url).get_data(as_text=True)

# A range containing 1.0 finds the curve and a disjoint range does not:
# the pair is what shows that the constraint reaches the query at all.
t = self.tc.get('/EllipticCurve/?field=2.2.5.1&szpiro_ratio=0.5-1.5').get_data(as_text=True)
assert curve_url in t
t = self.tc.get('/EllipticCurve/?field=2.2.5.1&szpiro_ratio=1.1-1.5').get_data(as_text=True)
assert curve_url not in t

# Sorting by the ratio works, and shows the column even though it is
# off by default: without the sort its results-table header carries
# display:none (it is always in the html, for the column selector).
L = self.tc.get('/EllipticCurve/?field=2.2.5.1&sort_order=szpiro_ratio')
assert L.status_code == 200
th = re.search(r'<th class="col-szpiro_ratio" style="([^"]*)"', L.get_data(as_text=True))
assert th is not None, 'no Szpiro ratio column in the results table'
assert 'display:none' not in th.group(1), th.group(0)

def test_szpiro_ratio_generator(self):
r"""
Test the helpers of scripts/ecnf/generate_szpiro_ratio.py, which
do not depend on ec_nfcurves having the szpiro_ratio column
"""
gen = szpiro_generator()
if gen is None:
self.skipTest("scripts/ecnf/generate_szpiro_ratio.py is not in this checkout")
# 2.2.5.1-31.1-a1: Norm(D_min) = Norm(N) = 31, so sigma = 1 exactly.
assert gen.szpiro_ratio(31, 31) == 1.0
# Everywhere good reduction: both norms are 1 and sigma is undefined.
assert gen.szpiro_ratio(1, 1) is None
# Trivial conductor with nontrivial minimal discriminant is impossible.
with self.assertRaises(ValueError):
gen.szpiro_ratio(2, 1)
with self.assertRaises(ValueError):
gen.szpiro_ratio(0, 31)

# A synthetic non-minimal row, modelled on 2.0.31.1-256.7-a1: the
# stored model is non-minimal at (2,w), so its discriminant norm is
# normp^12 = 2^12 times the norm of the minimal discriminant, and both
# formulas must give Norm(D_min) = 2^4 * 2^10.
rec = {'label': 'test.curve',
'non_min_p': ['(2,w)'],
'local_data': [{'p': '(2,w)', 'normp': 2, 'ord_disc': 4},
{'p': '(2,w+1)', 'normp': 2, 'ord_disc': 10}],
'normdisc': -2**26}
assert gen.min_disc_norm(rec) == 2**14
assert gen.min_disc_norm_from_normdisc(rec) == 2**14

# Local data missing for a prime listed in non_min_p: say which curve
# and which prime rather than failing on a StopIteration.
broken = dict(rec, local_data=rec['local_data'][1:])
with self.assertRaises(ValueError) as cm:
gen.min_disc_norm_from_normdisc(broken)
assert 'test.curve' in str(cm.exception) and '(2,w)' in str(cm.exception)

# normdisc not divisible by normp^12 at a non-minimal prime.
broken = dict(rec, normdisc=-(2**26 + 1))
with self.assertRaises(ValueError) as cm:
gen.min_disc_norm_from_normdisc(broken)
assert 'test.curve' in str(cm.exception)

def test_cm_disc_search(self):
r"""
Test that searching for CM field discriminant works
Expand Down
Loading
Loading