Skip to content

Commit d97585e

Browse files
roed314claude
andcommitted
shimura_curves: accept starred names in the jump box, add route tests
The jump box split its input on "*" before trying to recognize it, so starred curve names like X*(6;1) could never be found, and the canonical spelling X^*(6;1) was not accepted by NAME_RE at all. Now: - the whole input is first tried as a single label or standard name, and only then is "*" treated as a fiber product separator (splitting only at asterisks not followed by "(", so starred factors survive); - NAME_RE and shimcurve_lmfdb_label accept both X*(D;N) and X^*(D;N) (plus lowercase x), canonicalized via canonicalize_name to the X^*(D;N) form stored in the name column; - the fiber product branch no longer queries the nonexistent factorization column of gps_shimura_test (which raised a ValueError); the modular-curve X(1) special case is replaced by its analogue here, dropping index-1 factors with the same ambient (discB, discO, level, deg_mu), so e.g. X(6;1)*X^*(6;1) resolves to X(6;1), and unresolvable or malformed products flash a clean error instead of a 500. Add route-level tests for label/name/starred-name jumps, resolving and failing fiber products, malformed input, the search page, the starred family search, and individual curve pages, with content checked against gps_shimura_test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0c8d2be commit d97585e

2 files changed

Lines changed: 186 additions & 22 deletions

File tree

lmfdb/shimura_curves/main.py

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
fine_label_re = r"\d+\.\d+\.(?:\d+\.)?\d+\.\d+\.\d+-\d+\.\d+\.[a-z]+\.\d+\.\d+"
6060
LABEL_RE = re.compile(f"({coarse_label_re})|({fine_label_re})")
6161
FINE_LABEL_RE = re.compile(fine_label_re)
62-
NAME_RE = re.compile(r"X\*?\(\d+(,\d+)?(;|,)\d+\)")
62+
# Accepts the standard names X(D;N), X(D,M;N) and the starred (Atkin-Lehner
63+
# quotient) names in both spellings X*(D;N) and X^*(D;N); canonicalize_name
64+
# turns all of them into the form stored in the database.
65+
NAME_RE = re.compile(r"X(\^?\*)?\(\d+(,\d+)?(;|,)\d+\)")
6366

6467
def learnmore_list():
6568
return [('Source and acknowledgments', url_for(".how_computed_page")),
@@ -234,46 +237,81 @@ def shimcurve_lmfdb_label(label):
234237
lmfdb_label = label
235238
elif NAME_RE.fullmatch(label.upper()):
236239
label_type = "name"
237-
lmfdb_label = db.gps_shimura_test.lucky({"name": canonicalize_name(label)}, "label")
240+
# canonicalize_name maps both starred spellings X*(D;N) and X^*(D;N)
241+
# to the canonical form X^*(D;N) used in the name column
242+
lmfdb_label = db.gps_shimura_test.lucky({"name": canonicalize_name(label.upper())}, "label")
238243
else:
239244
label_type = "label"
240245
lmfdb_label = None
241246
return lmfdb_label, label_type
242247

243248
def shimcurve_jump(info):
244-
labels = (info["jump"]).split("*")
249+
jump = info["jump"].strip()
250+
# Starred names such as X*(6;1) or X^*(6;1) contain an asterisk that is
251+
# part of the name, so we first try to interpret the whole input as a
252+
# single label or name before treating * as a fiber product separator.
253+
lmfdb_label, label_type = shimcurve_lmfdb_label(jump)
254+
if lmfdb_label is not None:
255+
return redirect(url_for_shimcurve_label(lmfdb_label))
256+
if label_type == "name":
257+
# A syntactically valid name (possibly starred) that is not in the
258+
# database; the asterisk is not a fiber product separator here.
259+
flash_error("There is no Shimura curve in the database with name %s", jump)
260+
return redirect(url_for(".index"))
261+
# Now interpret * as a fiber product separator. An asterisk that is part
262+
# of a starred name is always followed by an open parenthesis, so we only
263+
# split at asterisks that are not.
264+
labels = [piece.strip() for piece in re.split(r"\*(?!\()", jump)]
265+
labels = [piece for piece in labels if piece]
266+
if len(labels) <= 1:
267+
flash_error("There is no Shimura curve in the database with %s %s", label_type, jump)
268+
return redirect(url_for(".index"))
245269
lmfdb_labels = []
246270
for label in labels:
247271
lmfdb_label, label_type = shimcurve_lmfdb_label(label)
248272
if lmfdb_label is None:
249273
flash_error("There is no Shimura curve in the database with %s %s", label_type, label)
250274
return redirect(url_for(".index"))
251275
lmfdb_labels.append(lmfdb_label)
252-
lmfdb_labels_not_X1 = [l for l in lmfdb_labels if l != "1.1.0.a.1"]
253-
if len(lmfdb_labels) == 1:
254-
label = lmfdb_labels[0]
255-
return redirect(url_for_shimcurve_label(label))
256-
elif len(lmfdb_labels_not_X1) == 1:
257-
label = lmfdb_labels_not_X1[0]
258-
return redirect(url_for_shimcurve_label(label))
259-
else:
276+
# Check that all the factors exist and get the data needed below
277+
curvedata = {rec["label"]: rec for rec in db.gps_shimura_test.search(
278+
{"label": {"$in": lmfdb_labels}},
279+
["label", "index", "discB", "discO", "level", "deg_mu"])}
280+
for label in lmfdb_labels:
281+
if label not in curvedata:
282+
flash_error("There is no Shimura curve in the database with label %s", label)
283+
return redirect(url_for(".index"))
284+
285+
def ambient(label):
286+
rec = curvedata[label]
287+
return (rec["discB"], rec["discO"], rec["level"], rec["deg_mu"])
288+
# Analogue of X(1) in the modular curve jump: a factor of index 1
289+
# corresponds to the full ambient group, so it is the base of the fiber
290+
# product and the fiber product with it changes nothing (as long as it has
291+
# the same ambient data as the other factor).
292+
nontrivial = [l for l in lmfdb_labels if curvedata[l]["index"] != 1]
293+
if len(nontrivial) == 1:
294+
label = nontrivial[0]
295+
if all(ambient(l) == ambient(label) for l in lmfdb_labels):
296+
return redirect(url_for_shimcurve_label(label))
297+
# Check labels are indeed distinct
298+
if len(set(nontrivial)) != len(nontrivial):
299+
flash_error("Fiber product decompositions cannot contain repeated terms")
300+
return redirect(url_for(".index"))
301+
label = None
302+
if nontrivial and "factorization" in db.gps_shimura_test.search_cols:
260303
# Get factorization for each label
261-
factors = list(db.gps_shimura_test.search({"label": {"$in": lmfdb_labels_not_X1}},
262-
["label","factorization"]))
304+
factors = list(db.gps_shimura_test.search({"label": {"$in": nontrivial}},
305+
["label", "factorization"]))
263306
factors = [(f["factorization"] if f["factorization"] != [] else [f["label"]])
264307
for f in factors]
265-
# Check labels are indeed distinct
266-
if len(factors) != len(lmfdb_labels_not_X1):
267-
flash_error("Fiber product decompositions cannot contain repeated terms")
268-
return redirect(url_for(".index"))
269308
# Get list of all factors, lexicographically sorted
270309
factors = sorted(sum(factors, []), key=key_for_numerically_sort)
271310
label = db.gps_shimura_test.lucky({'factorization': factors}, "label")
272-
if label is None:
273-
flash_error("There is no Shimura curve in the database isomorphic to the fiber product %s", info["jump"])
274-
return redirect(url_for(".index"))
275-
else:
276-
return redirect(url_for_shimcurve_label(label))
311+
if label is None:
312+
flash_error("There is no Shimura curve in the database isomorphic to the fiber product %s", jump)
313+
return redirect(url_for(".index"))
314+
return redirect(url_for_shimcurve_label(label))
277315

278316
def blankzeros(n):
279317
return "$%o$"%n if n else ""
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,138 @@
11
# -*- coding: utf-8 -*-
22

3+
from urllib.parse import quote
4+
35
from lmfdb.tests import LmfdbTest
46

7+
# These are checked against the database in the tests below
8+
STAR_NAME = "X^*(6;1)" # canonical spelling of the starred curve name
9+
STAR_LABEL = "6.1.1.1.0.a.1"
10+
PLAIN_NAME = "X(6;1)"
11+
PLAIN_LABEL = "6.1.1.4.0.a.1"
12+
13+
514
class ShimCrvTest(LmfdbTest):
15+
def jump(self, entry):
16+
return self.tc.get(
17+
"/ShimuraCurve/Q/?jump=%s" % quote(entry, safe=""),
18+
follow_redirects=True,
19+
)
20+
621
def test_home(self):
722
L = self.tc.get('/ShimuraCurve/Q/')
823
assert 'Shimura curves' in L.get_data(as_text=True)
924
assert 'Browse' in L.get_data(as_text=True)
1025
assert 'Search' in L.get_data(as_text=True)
1126
assert 'Find' in L.get_data(as_text=True)
1227
assert 'X(D;N)' in L.get_data(as_text=True)
28+
29+
def test_jump_label(self):
30+
# Jumping to an LMFDB label goes directly to the curve page
31+
rec = self.db.gps_shimura_test.lookup(STAR_LABEL, ["name", "label"])
32+
assert rec["name"] == STAR_NAME
33+
L = self.jump(STAR_LABEL)
34+
assert L.status_code == 200
35+
page = L.get_data(as_text=True)
36+
assert STAR_LABEL in page
37+
assert STAR_NAME in page
38+
39+
def test_jump_plain_name(self):
40+
# An unstarred standard name, in both X(D;N) and X(D,N) spellings
41+
assert self.db.gps_shimura_test.lucky({"name": PLAIN_NAME}, "label") == PLAIN_LABEL
42+
for entry in [PLAIN_NAME, "X(6,1)"]:
43+
L = self.jump(entry)
44+
assert L.status_code == 200
45+
page = L.get_data(as_text=True)
46+
assert PLAIN_LABEL in page
47+
assert PLAIN_NAME in page
48+
49+
def test_jump_starred_name(self):
50+
# Starred names contain an asterisk, which must not be treated as a
51+
# fiber product separator; both X*(D;N) and the canonical X^*(D;N)
52+
# spelling are accepted (as well as a lowercase x)
53+
assert self.db.gps_shimura_test.lucky({"name": STAR_NAME}, "label") == STAR_LABEL
54+
for entry in ["X*(6;1)", "X^*(6;1)", "x*(6;1)"]:
55+
L = self.jump(entry)
56+
assert L.status_code == 200
57+
page = L.get_data(as_text=True)
58+
assert STAR_LABEL in page, entry
59+
assert STAR_NAME in page, entry
60+
61+
def test_jump_fiber_product(self):
62+
# X^*(6;1) has index 1 (its group is the full ambient group), so the
63+
# fiber product of X(6;1) with it is X(6;1) itself; this can be
64+
# entered with names (including starred ones) or labels
65+
for entry in [
66+
"X(6;1)*X^*(6;1)",
67+
"X*(6;1)*X(6;1)",
68+
"%s*%s" % (PLAIN_LABEL, STAR_LABEL),
69+
]:
70+
L = self.jump(entry)
71+
assert L.status_code == 200
72+
page = L.get_data(as_text=True)
73+
assert PLAIN_LABEL in page, entry
74+
assert PLAIN_NAME in page, entry
75+
# A fiber product not isomorphic to any curve in the database
76+
# produces a clean error message
77+
assert self.db.gps_shimura_test.lucky({"name": "X(10;1)"}, "label") is not None
78+
L = self.jump("X(6;1)*X(10;1)")
79+
assert L.status_code == 200
80+
assert "There is no Shimura curve in the database isomorphic to the fiber product" in L.get_data(as_text=True)
81+
# Repeated factors are rejected with a clean error message
82+
L = self.jump("X(6;1)*X(6;1)")
83+
assert L.status_code == 200
84+
assert "Fiber product decompositions cannot contain repeated terms" in L.get_data(as_text=True)
85+
86+
def test_jump_malformed(self):
87+
# Malformed input (including malformed starred names) should flash an
88+
# error, not produce a 500
89+
for entry in ["X*(6;", "X^*(6;", "X*(6;1", "banana", "X(6;1)*banana"]:
90+
L = self.jump(entry)
91+
assert L.status_code == 200, entry
92+
assert "There is no Shimura curve in the database" in L.get_data(as_text=True), entry
93+
# A syntactically valid starred name that is not in the database
94+
L = self.jump("X*(10;1)")
95+
assert L.status_code == 200
96+
assert "There is no Shimura curve in the database with name" in L.get_data(as_text=True)
97+
98+
def test_search(self):
99+
# All discB = 6, level 1 curves should show up in a search
100+
expected = sorted(rec["label"] for rec in self.db.gps_shimura_test.search(
101+
{"discB": 6, "level": 1}, ["label"]))
102+
assert PLAIN_LABEL in expected and STAR_LABEL in expected
103+
L = self.tc.get("/ShimuraCurve/Q/?discB=6&level=1")
104+
assert L.status_code == 200
105+
page = L.get_data(as_text=True)
106+
for label in expected:
107+
assert label in page
108+
# names are displayed (in LaTeX form) in the search results
109+
assert STAR_NAME in page
110+
assert PLAIN_NAME in page
111+
112+
def test_search_family(self):
113+
# Searching for the starred family X^*(D;N) should find the starred curve
114+
starred = sorted(rec["label"] for rec in self.db.gps_shimura_test.search(
115+
{"name": {"$like": "X^*(%"}}, ["label"]))
116+
assert STAR_LABEL in starred
117+
L = self.tc.get("/ShimuraCurve/Q/?family=XDNstar")
118+
assert L.status_code == 200
119+
page = L.get_data(as_text=True)
120+
for label in starred:
121+
assert label in page
122+
123+
def test_curve_page(self):
124+
# Check content of individual curve pages against the database
125+
for label in [PLAIN_LABEL, STAR_LABEL]:
126+
rec = self.db.gps_shimura_test.lookup(
127+
label, ["name", "genus", "index", "discB", "level"])
128+
L = self.tc.get("/ShimuraCurve/Q/%s/" % label)
129+
assert L.status_code == 200
130+
page = L.get_data(as_text=True)
131+
assert "Shimura curve $%s$" % rec["name"] in page
132+
assert label in page
133+
# invariants from the database: genus (as displayed by show_genus),
134+
# index and discriminant
135+
assert "$ %s " % rec["genus"] in page
136+
assert "$%s$" % rec["index"] in page
137+
assert "Discriminant of $B$" in page
138+
assert "$%s$" % rec["discB"] in page

0 commit comments

Comments
 (0)