Skip to content

Commit 24eff26

Browse files
roed314claude
andcommitted
Speed up Dirichlet character searches using mathematical relations (LMFDB#6733, LMFDB#6422)
With the new indexes, remaining production timeouts come from an indexed column combined with a sparse boolean filter, e.g. conductor=17 & is_primitive=yes walks all 230K conductor-17 rows to find the 4 primitive ones (>180s on devmirror). Since a character is primitive if and only if its conductor equals its modulus, mirror conductor/modulus constraints when is_primitive=yes so the query is answered from the (conductor, modulus, orbit) index (45ms). Searches for characters induced by psi now inherit psi's order, parity and realness, and searches that are provably empty (odd order with odd parity, is_real with order > 2, conductor not dividing modulus, etc.) short-circuit with an explanatory message instead of scanning the table. Verified with EXPLAIN ANALYZE on devmirror and new tests in lmfdb/characters/test_characters.py (3 passed); pyflakes clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5ef81bd commit 24eff26

2 files changed

Lines changed: 216 additions & 15 deletions

File tree

lmfdb/characters/main.py

Lines changed: 171 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from lmfdb.utils import (
77
to_dict, flash_error, SearchArray, YesNoBox, display_knowl, ParityBox,
88
TextBox, CountBox, parse_bool, parse_ints, search_wrap, raw_typeset_poly,
9-
StatsDisplay, totaler, proportioners, comma, flash_warning, Downloader, redirect_no_cache, CodeSnippet)
9+
StatsDisplay, totaler, proportioners, comma, flash_warning, flash_info,
10+
Downloader, redirect_no_cache, CodeSnippet)
1011
from lmfdb.utils.interesting import interesting_knowls
1112
from lmfdb.utils.search_parsing import parse_range3
1213
from lmfdb.utils.search_columns import SearchColumns, MathCol, LinkCol, CheckCol, ProcessedCol, MultiProcessedCol
@@ -158,10 +159,129 @@ def search_types(self, info):
158159
('Random', 'Random character')])
159160

160161

162+
def int_bounds(val):
163+
"""
164+
The lower and upper bounds implied by a query value produced by
165+
``parse_ints`` (either an integer or a dictionary with keys among
166+
``$gte`` and ``$lte``). Either bound may be ``None`` (unbounded), and
167+
``(None, None)`` is returned for values of any other shape.
168+
"""
169+
if isinstance(val, int):
170+
return val, val
171+
if isinstance(val, dict) and val and all(key in ["$gte", "$lte"] for key in val):
172+
return val.get("$gte"), val.get("$lte")
173+
return None, None
174+
175+
176+
def forced_int(val):
177+
"""
178+
The unique integer allowed by a query value produced by ``parse_ints``,
179+
or ``None`` if the value does not pin down a single integer.
180+
"""
181+
lo, hi = int_bounds(val)
182+
return lo if lo is not None and lo == hi else None
183+
184+
185+
def force_no_results(info, query, reason, *args):
186+
"""
187+
Used when the search constraints are contradictory for mathematical
188+
reasons. We tell the user why there are no results and replace the query
189+
with one that provably returns nothing but is answered instantly from the
190+
hash index on label, rather than letting postgres scan a large part of
191+
the table to find nothing (see issues #6733 and #6422).
192+
"""
193+
if "result_count" not in info:
194+
flash_info("This search returns no results because " + reason + ".", *args)
195+
query.clear()
196+
query["label"] = ""
197+
return True
198+
199+
200+
def refine_primitive_search(info, query):
201+
"""
202+
A character is primitive if and only if its conductor equals its modulus.
203+
When ``is_primitive=yes``, mirroring a constraint on one of
204+
modulus/conductor onto the other column lets postgres answer searches
205+
like ``conductor=17&is_primitive=yes`` from the (conductor, modulus,
206+
orbit) index instead of scanning every character of conductor 17 and
207+
filtering (issue #6733). Returns True if the query was found to be
208+
contradictory (and was replaced by an empty one).
209+
"""
210+
def mirrorable(val):
211+
return int_bounds(val) != (None, None)
212+
213+
def copy_value(val):
214+
return dict(val) if isinstance(val, dict) else val
215+
216+
con = query.get("conductor")
217+
mod = query.get("modulus")
218+
if con is not None and mod is None:
219+
if mirrorable(con):
220+
query["modulus"] = copy_value(con)
221+
if isinstance(con, int):
222+
# conductor = modulus already forces primitivity
223+
query.pop("is_primitive")
224+
elif mod is not None and con is None:
225+
if mirrorable(mod):
226+
query["conductor"] = copy_value(mod)
227+
if isinstance(mod, int):
228+
query.pop("is_primitive")
229+
elif con is not None and mod is not None and mirrorable(con) and mirrorable(mod):
230+
clo, chi = int_bounds(con)
231+
mlo, mhi = int_bounds(mod)
232+
lo = clo if mlo is None else (mlo if clo is None else max(clo, mlo))
233+
hi = chi if mhi is None else (mhi if chi is None else min(chi, mhi))
234+
if lo is not None and hi is not None and lo > hi:
235+
return force_no_results(info, query, "a primitive character has modulus equal to its conductor")
236+
if lo is not None and lo == hi:
237+
query["conductor"] = query["modulus"] = lo
238+
query.pop("is_primitive")
239+
else:
240+
merged = {}
241+
if lo is not None:
242+
merged["$gte"] = lo
243+
if hi is not None:
244+
merged["$lte"] = hi
245+
query["conductor"] = merged
246+
query["modulus"] = dict(merged)
247+
# Constraints coming from comma separated inputs are stored inside $or;
248+
# mirror each branch separately, dropping impossible branches.
249+
ors = query.get("$or")
250+
if query.get("is_primitive") is True and isinstance(ors, list):
251+
newors = []
252+
for branch in ors:
253+
bcon = branch.get("conductor")
254+
bmod = branch.get("modulus")
255+
if bcon is not None and bmod is None and mirrorable(bcon):
256+
branch = dict(branch)
257+
branch["modulus"] = copy_value(bcon)
258+
elif bmod is not None and bcon is None and mirrorable(bmod):
259+
branch = dict(branch)
260+
branch["conductor"] = copy_value(bmod)
261+
else:
262+
fcon = forced_int(bcon)
263+
fmod = forced_int(bmod)
264+
if fcon is not None and fmod is not None and fcon != fmod:
265+
continue
266+
newors.append(branch)
267+
if not newors:
268+
return force_no_results(info, query, "a primitive character has modulus equal to its conductor")
269+
query["$or"] = newors
270+
271+
161272
def common_parse(info, query):
162273
parse_ints(info, query, "modulus", name="modulus")
163274
parse_ints(info, query, "conductor", name="conductor")
164275
parse_ints(info, query, "order", name="order")
276+
if 'parity' in info:
277+
parity = info['parity']
278+
if parity == 'even':
279+
query['is_even'] = True
280+
elif parity == 'odd':
281+
query['is_even'] = False
282+
parse_bool(info, query, "is_primitive", name="is_primitive")
283+
parse_bool(info, query, "is_real", name="is_real")
284+
parse_bool(info, query, "is_minimal", name="is_minimal")
165285
if 'inducing' in info:
166286
try:
167287
validate_label(info['inducing'])
@@ -174,7 +294,8 @@ def common_parse(info, query):
174294
parts_of_label = label.split(".")
175295
primitive_modulus = int(parts_of_label[0])
176296
primitive_orbit = class_to_int(parts_of_label[1])+1
177-
if db.char_dirichlet.count({'modulus':primitive_modulus,'is_primitive':True,'orbit':primitive_orbit}) == 0:
297+
psi = db.char_dirichlet.lucky({'modulus':primitive_modulus,'is_primitive':True,'orbit':primitive_orbit}, projection=["order", "is_even"])
298+
if psi is None:
178299
raise ValueError("Primitive character orbit not found")
179300

180301
def incompatible(query):
@@ -190,22 +311,57 @@ def incompatible(query):
190311
return False
191312
return True
192313
if incompatible(query):
193-
query["primitive_orbit"] = 0
194-
else:
195-
query["conductor"] = primitive_modulus
196-
query["primitive_orbit"] = primitive_orbit
314+
return force_no_results(info, query, "a character induced by %s has conductor %s", info['inducing'], primitive_modulus)
315+
query["conductor"] = primitive_modulus
316+
query["primitive_orbit"] = primitive_orbit
317+
# A character induced by psi has the same order, parity and
318+
# realness as psi; recording this catches contradictory searches
319+
# and lets postgres use the (order, conductor, modulus, orbit)
320+
# index instead of filtering all characters of this conductor.
321+
olo, ohi = int_bounds(query["order"]) if "order" in query else (None, None)
322+
if (olo is not None and olo > psi["order"]) or (ohi is not None and ohi < psi["order"]):
323+
return force_no_results(info, query, "a character induced by %s has order %s", info['inducing'], psi["order"])
324+
query["order"] = psi["order"]
325+
if query.get("is_even") == (not psi["is_even"]):
326+
return force_no_results(info, query, "a character induced by %s is %s", info['inducing'], "even" if psi["is_even"] else "odd")
327+
query.pop("is_even", None)
328+
psi_real = psi["order"] <= 2
329+
if query.get("is_real") == (not psi_real):
330+
return force_no_results(info, query, "a character induced by %s is %s", info['inducing'], "real" if psi_real else "not real")
331+
query.pop("is_real", None)
197332
except ValueError:
198333
flash_error("%s is not the label of a primitive character in the database", info['inducing'])
199334
raise ValueError
200-
if 'parity' in info:
201-
parity = info['parity']
202-
if parity == 'even':
203-
query['is_even'] = True
204-
elif parity == 'odd':
205-
query['is_even'] = False
206-
parse_bool(info, query, "is_primitive", name="is_primitive")
207-
parse_bool(info, query, "is_real", name="is_real")
208-
parse_bool(info, query, "is_minimal", name="is_minimal")
335+
# Mathematical facts relating the search columns let us short-circuit
336+
# or speed up several searches that postgres would otherwise answer by
337+
# filtering a large part of the table (issues #6733 and #6422).
338+
con = forced_int(query.get("conductor"))
339+
mod = forced_int(query.get("modulus"))
340+
if con is not None and mod is not None:
341+
if con > 0 and mod % con != 0:
342+
return force_no_results(info, query, "the conductor of a character divides its modulus, so there are no characters of modulus %s and conductor %s", mod, con)
343+
if query.get("is_primitive") is False and con == mod:
344+
return force_no_results(info, query, "an imprimitive character has conductor strictly smaller than its modulus")
345+
if query.get("is_primitive") is True:
346+
if refine_primitive_search(info, query):
347+
return
348+
if "order" in query:
349+
olo, ohi = int_bounds(query["order"])
350+
is_real = query.get("is_real")
351+
if is_real is True and olo is not None and olo > 2:
352+
return force_no_results(info, query, "a real character has order at most 2")
353+
if is_real is False and ohi is not None and ohi <= 2:
354+
return force_no_results(info, query, "every character of order at most 2 is real")
355+
if (is_real is True and ohi is not None and ohi <= 2) or (is_real is False and olo is not None and olo > 2):
356+
# the order constraint already forces the requested realness
357+
del query["is_real"]
358+
oforced = forced_int(query["order"])
359+
if oforced is not None and oforced % 2 == 1:
360+
# a character of odd order takes the value 1 at -1
361+
if query.get("is_even") is False:
362+
return force_no_results(info, query, "a character of odd order is even, so there are no odd characters of order %s", oforced)
363+
if query.get("is_even") is True:
364+
del query["is_even"]
209365

210366

211367
def validate_label(label):

lmfdb/characters/test_characters.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,51 @@ def test_condsearch(self):
5252
W = self.tc.get('/Character/Dirichlet/?conductor=111')
5353
assert '111.m' in W.get_data(as_text=True)
5454

55+
def test_primitive_search(self):
56+
# A primitive character has modulus equal to its conductor, which is
57+
# used to answer these searches from the (conductor, modulus, orbit)
58+
# index; see issue #6733
59+
W = self.tc.get('/Character/Dirichlet/?conductor=17&is_primitive=yes')
60+
data = W.get_data(as_text=True)
61+
assert 'Results (4 matches)' in data
62+
for label in ['17.b', '17.c', '17.d', '17.e']:
63+
assert label in data
64+
W = self.tc.get('/Character/Dirichlet/?conductor=17,61&is_primitive=yes')
65+
data = W.get_data(as_text=True)
66+
assert '17.e' in data and '61.b' in data
67+
W = self.tc.get('/Character/Dirichlet/?modulus=61&is_primitive=yes')
68+
assert '61.h' in W.get_data(as_text=True)
69+
W = self.tc.get('/Character/Dirichlet/?conductor=17&modulus=10-20&is_primitive=yes')
70+
assert '17.e' in W.get_data(as_text=True)
71+
72+
def test_contradictory_search(self):
73+
# Searches that provably have no results are answered without
74+
# scanning the table; see issues #6733 and #6422
75+
queries = ['order=3&parity=odd', # odd order forces even parity
76+
'order=3&parity=odd&is_primitive=yes&is_real=no',
77+
'is_real=yes&order=5', # real characters have order at most 2
78+
'is_real=no&order=1-2',
79+
'modulus=100&conductor=17', # conductor divides modulus
80+
'modulus=17&conductor=17&is_primitive=no',
81+
'conductor=17&modulus=18-20&is_primitive=yes',
82+
'inducing=3.b&order=3', # 3.b has order 2
83+
'inducing=3.b&parity=even', # 3.b is odd
84+
'inducing=3.b&is_real=no', # 3.b is real
85+
'inducing=3.b&conductor=5'] # induced characters have conductor 3
86+
for q in queries:
87+
W = self.tc.get('/Character/Dirichlet/?' + q)
88+
data = W.get_data(as_text=True)
89+
assert 'This search returns no results because' in data, q
90+
assert 'No matches' in data, q
91+
92+
def test_inducing_search(self):
93+
W = self.tc.get('/Character/Dirichlet/?inducing=3.b&modulus=1-100')
94+
data = W.get_data(as_text=True)
95+
assert '6.b' in data and '15.c' in data
96+
# compatible constraints on inherited quantities are harmless
97+
W = self.tc.get('/Character/Dirichlet/?inducing=3.b&modulus=1-100&order=2&parity=odd&is_real=yes')
98+
assert '15.c' in W.get_data(as_text=True)
99+
55100
def test_nextprev(self):
56101
W = self.tc.get('/Character/Dirichlet/?start=200&count=25&order=3')
57102
assert r'288.i' in W.get_data(as_text=True)

0 commit comments

Comments
 (0)