diff --git a/lmfdb/abvar/fq/isog_class.py b/lmfdb/abvar/fq/isog_class.py
index 4a93cbca8f..188ecb1438 100644
--- a/lmfdb/abvar/fq/isog_class.py
+++ b/lmfdb/abvar/fq/isog_class.py
@@ -84,6 +84,11 @@ def __init__(self, dbdata):
dbdata["hyp_count"] = None
if "jacobian_count" not in dbdata:
dbdata["jacobian_count"] = None
+ # New invariants: cyclicity and noncyclic primes
+ if "is_cyclic" not in dbdata:
+ dbdata["is_cyclic"] = None
+ if "noncyclic_primes" not in dbdata:
+ dbdata["noncyclic_primes"] = []
self.__dict__.update(dbdata)
@classmethod
@@ -232,9 +237,15 @@ def properties(self):
("Primitive", "yes" if self.is_primitive else "no"),
]
if self.has_principal_polarization != 0:
- props += [("Principally polarizable", "yes" if self.has_principal_polarization == 1 else "no")]
+ props += [(
+ "Principally polarizable",
+ "yes" if self.has_principal_polarization == 1 else "no",
+ )]
if self.has_jacobian != 0:
- props += [("Contains a Jacobian", "yes" if self.has_jacobian == 1 else "no")]
+ props += [(
+ "Contains a Jacobian",
+ "yes" if self.has_jacobian == 1 else "no",
+ )]
return props
# at some point we were going to display the weil_numbers instead of the frobenius angles
diff --git a/lmfdb/abvar/fq/main.py b/lmfdb/abvar/fq/main.py
index 52c81732ec..981d910d85 100644
--- a/lmfdb/abvar/fq/main.py
+++ b/lmfdb/abvar/fq/main.py
@@ -23,6 +23,7 @@
from lmfdb.utils import redirect_no_cache
from lmfdb.utils.search_columns import SearchColumns, SearchCol, MathCol, LinkCol, ProcessedCol, CheckCol, CheckMaybeCol
from lmfdb.abvar.fq.download import AbvarFq_download
+from lmfdb.utils.search_parsing import parse_primes
logger = make_logger("abvarfq")
@@ -399,8 +400,31 @@ def nbsp(knowl, label):
jacobian = YesNoMaybeBox(
"jacobian",
label="Jacobian",
- knowl="ag.jacobian"
+ knowl="ag.jacobian",
)
+
+ # Cyclic group of points (advanced yes/no box)
+ cyclic = YesNoBox(
+ "cyclic",
+ label="Cyclic group of points",
+ knowl="av.fq.cyclic_group_points",
+ advanced=True,
+ )
+
+ # Non-cyclic primes with mode selector (include / exactly / subset)
+ noncyclic_mode = SubsetBox(
+ "noncyclic_primes_mode",
+ advanced=True,
+ )
+ noncyclic_primes = TextBoxWithSelect(
+ "noncyclic_primes",
+ label="Non-cyclic primes",
+ select_box=noncyclic_mode,
+ knowl="av.fq.noncyclic_primes",
+ example="2 or 2,3,5",
+ advanced=True,
+ )
+
uglabel = "Use %s in the following inputs" % display_knowl("av.decomposition", "Geometric decomposition")
use_geom_decomp = CheckBox(
"use_geom_decomp",
@@ -506,6 +530,7 @@ def short_label(d):
[newton_polygon, abvar_point_count, curve_point_count, simple_factors],
[newton_elevation, jac_cnt, hyp_cnt, twist_count, max_twist_degree],
[angle_rank, angle_corank, geom_deg, p_corank, geom_squarefree],
+ [cyclic, noncyclic_primes],
use_geom_refine,
[dim1, dim2, dim3, dim4, dim5],
[dim1d, dim2d, dim3d, number_field, galois_group],
@@ -516,6 +541,7 @@ def short_label(d):
[g, geom_simple],
[initial_coefficients, polarizable],
[p_rank, jacobian],
+ [cyclic, noncyclic_primes],
[p_corank, geom_squarefree],
[jac_cnt, hyp_cnt],
[angle_rank, angle_corank],
@@ -552,6 +578,7 @@ def common_parse(info, query):
parse_bool(info, query, "primitive", qfield="is_primitive")
parse_bool_unknown(info, query, "jacobian", qfield="has_jacobian")
parse_bool_unknown(info, query, "polarizable", qfield="has_principal_polarization")
+ parse_bool(info, query, "cyclic", qfield="is_cyclic")
parse_ints(info, query, "p_rank")
parse_ints(info, query, "p_corank", qfield="p_rank_deficit")
parse_ints(info, query, "angle_rank")
@@ -561,6 +588,14 @@ def common_parse(info, query):
parse_ints(info, query, "hyp_cnt", qfield="hyp_count", name="Number of Hyperelliptic Jacobians")
parse_ints(info, query, "twist_count")
parse_ints(info, query, "max_twist_degree")
+ parse_primes(
+ info,
+ query,
+ "noncyclic_primes",
+ qfield="noncyclic_primes",
+ mode=info.get("noncyclic_primes_mode"),
+ )
+
parse_ints(info, query, "size")
parse_newton_polygon(info, query, "newton_polygon", qfield="slopes")
parse_string_start(info, query, "initial_coefficients", qfield="poly_str", initial_segment=["1"])
@@ -680,7 +715,7 @@ def extended_code(c):
ProcessedCol("number_fields", "av.fq.number_field", "Number fields", lambda nfs: ", ".join(nf_display_knowl(nf, field_pretty(nf)) for nf in nfs), default=False),
SearchCol("galois_groups_pretty", "nf.galois_group", "Galois groups", download_col="galois_groups", default=False),
SearchCol("decomposition_display_search", "av.decomposition", "Isogeny factors", download_col="decompositionraw")],
- db_cols=["label", "g", "q", "poly", "p_rank", "p_rank_deficit", "is_simple", "is_geometrically_simple", "simple_distinct", "simple_multiplicities", "is_primitive", "primitive_models", "curve_count", "curve_counts", "abvar_count", "abvar_counts", "jacobian_count", "hyp_count", "number_fields", "galois_groups", "slopes", "newton_elevation", "twist_count", "max_twist_degree", "geometric_extension_degree", "angle_rank", "angle_corank", "is_supersingular", "has_principal_polarization", "has_jacobian"])
+ db_cols=["label", "g", "q", "poly", "p_rank", "p_rank_deficit", "is_simple", "is_geometrically_simple", "simple_distinct", "simple_multiplicities", "is_primitive", "primitive_models", "curve_count", "curve_counts", "abvar_count", "abvar_counts", "jacobian_count", "hyp_count", "number_fields", "galois_groups", "slopes", "newton_elevation", "twist_count", "max_twist_degree", "geometric_extension_degree", "angle_rank", "angle_corank", "is_supersingular", "has_principal_polarization", "has_jacobian", "is_cyclic", "noncyclic_primes"])
def abvar_postprocess(res, info, query):
gals = set()
diff --git a/lmfdb/abvar/fq/templates/show-abvarfq.html b/lmfdb/abvar/fq/templates/show-abvarfq.html
index bc8290a69f..ae6986b148 100644
--- a/lmfdb/abvar/fq/templates/show-abvarfq.html
+++ b/lmfdb/abvar/fq/templates/show-abvarfq.html
@@ -31,6 +31,27 @@
Invariants
{% if cl.size is not none %}
| {{KNOWL('av.fq.isogeny_class_size',title='Isomorphism classes')}}: | {{cl.size}} |
{% endif %}
+ {# --- NEW: cyclicity invariants --- #}
+ {% if cl.is_cyclic is not none %}
+
+ | {{ KNOWL('av.fq.cyclic_group_points', title='Cyclic group of points') }}: |
+
+ {% if cl.is_cyclic %}
+ yes
+ {% else %}
+ no
+ {% endif %}
+ |
+
+ {% endif %}
+
+
+ {% if cl.noncyclic_primes %}
+
+ | {{ KNOWL('av.fq.noncyclic_primes', title='Non-cyclic primes') }}: |
+ ${{ cl.noncyclic_primes|join(', ') }}$ |
+
+ {% endif %}
diff --git a/lmfdb/abvar/fq/test_av.py b/lmfdb/abvar/fq/test_av.py
index e7c4cf0ca6..4ac2c21260 100644
--- a/lmfdb/abvar/fq/test_av.py
+++ b/lmfdb/abvar/fq/test_av.py
@@ -157,3 +157,11 @@ def test_download_curves(self):
page = self.tc.get('Variety/Abelian/Fq/download_curves/5.3.ac_e_ai_v_abl', follow_redirects=True)
assert 'No curves for abelian variety isogeny class 5.3.ac_e_ai_v_abl' in page.get_data(as_text=True)
+
+ def test_cyclic_group_of_points_display(self):
+ r"""
+ Check that the cyclic group of points information is displayed
+ on the isogeny-class page.
+ """
+ page = self.tc.get("/Variety/Abelian/Fq/2/9/aj_bl").get_data(as_text=True)
+ assert "Cyclic" in page
diff --git a/lmfdb/abvar/fq/test_browse_page.py b/lmfdb/abvar/fq/test_browse_page.py
index 55ac1771f7..faba6d6ab2 100644
--- a/lmfdb/abvar/fq/test_browse_page.py
+++ b/lmfdb/abvar/fq/test_browse_page.py
@@ -12,9 +12,10 @@ def test_index_page(self):
"""
homepage = self.tc.get("/Variety/Abelian/Fq/").get_data(as_text=True)
assert "by dimension and base field" in homepage
+ assert "Cyclic group of points" in homepage
def test_stats_page(self):
- self.check_args("/Variety/Abelian/Fq/stats","Abelian variety isogeny classes: Statistics")
+ self.check_args("/Variety/Abelian/Fq/stats", "Abelian variety isogeny classes: Statistics")
# TODO test dynamic stats
@@ -43,7 +44,10 @@ def test_lookup(self):
r"""
Check that Variety/Abelian/Fq/?jump works
"""
- self.check_args("/Variety/Abelian/Fq/?jump=x^6-3*x^5%2B3*x^4-2*x^3%2B6*x^2-12*x%2B8", "3.2.ad_d_ac")
+ self.check_args(
+ "/Variety/Abelian/Fq/?jump=x^6-3*x^5%2B3*x^4-2*x^3%2B6*x^2-12*x%2B8",
+ "3.2.ad_d_ac"
+ )
# Various searches
# Many things are checked twice: Once from main index/browse page, and once from the refining search page
@@ -148,7 +152,10 @@ def test_search_newton(self):
# slope not a rational number
self.check_args("/Variety/Abelian/Fq/?newton_polygon=t", "is not a valid input")
# slopes are not increasing
- self.check_args("/Variety/Abelian/Fq/?start=&count=&newton_polygon=%5B1%2C1%2F2%2C0%5D", "Slopes must be increasing")
+ self.check_args(
+ "/Variety/Abelian/Fq/?start=&count=&newton_polygon=%5B1%2C1%2F2%2C0%5D",
+ "Slopes must be increasing"
+ )
def test_search_initcoeffs(self):
r"""
@@ -157,7 +164,10 @@ def test_search_initcoeffs(self):
self.check_args("/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D", "4.3.b_ab_d_j")
self.check_args("/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D", "4.3.b_ab_d_j")
# there should be only one match, if ranges were supported
- self.check_args("/Variety/Abelian/Fq/?angle_ranks=&initial_coefficients=%5B3%2C+9%2C+10%2C+87-100%5D", "Ranges not supported")
+ self.check_args(
+ "/Variety/Abelian/Fq/?angle_ranks=&initial_coefficients=%5B3%2C+9%2C+10%2C+87-100%5D",
+ "Ranges not supported"
+ )
def test_search_pointcountsav(self):
r"""
@@ -187,8 +197,14 @@ def test_search_isogfactor(self):
Check that we can search by decomposition into isogeny factors
"""
# [3.5.ah_y_ach,*]
- self.check_args("/Variety/Abelian/Fq/?simple_quantifier=include&simple_factors=3.5.ah_y_ach", "4.5.ak_by_agk_qb")
- self.check_args("/Variety/Abelian/Fq/?p_rank=4&dim1_factors=2&dim2_factors=2&dim1_distinct=1&dim2_distinct=1", "6.2.ag_p_aw_bh_acu_ey")
+ self.check_args(
+ "/Variety/Abelian/Fq/?simple_quantifier=include&simple_factors=3.5.ah_y_ach",
+ "4.5.ak_by_agk_qb"
+ )
+ self.check_args(
+ "/Variety/Abelian/Fq/?p_rank=4&dim1_factors=2&dim2_factors=2&dim1_distinct=1&dim2_distinct=1",
+ "6.2.ag_p_aw_bh_acu_ey"
+ )
self.check_args("/Variety/Abelian/Fq/?dim1_factors=6&dim1_distinct=1", "5 matches")
def test_search_numberfield(self):
@@ -274,8 +290,51 @@ def test_search_combos(self):
self.check_args("/Variety/Abelian/Fq/?p_rank=2&initial_coefficients=%5B1%2C-1%2C3%2C9%5D", "4.3.b_ab_d_j")
self.check_args("/Variety/Abelian/Fq/?p_rank=2&initial_coefficients=%5B1%2C-1%2C3%2C9%5D", "4.3.b_ab_d_j")
# initial coefficients and point counts of the abelian variety
- self.check_args("/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D&abvar_point_count=%5B75%2C7125%5D", "No matches")
- self.check_args("/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D&abvar_point_count=%5B75%2C7125%5D", "No matches")
+ self.check_args(
+ "/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D&abvar_point_count=%5B75%2C7125%5D",
+ "No matches"
+ )
+ self.check_args(
+ "/Variety/Abelian/Fq/?initial_coefficients=%5B1%2C-1%2C3%2C9%5D&abvar_point_count=%5B75%2C7125%5D",
+ "No matches"
+ )
# Combining unknown fields on Jacobian and Principal polarization.
self.check_args("/Variety/Abelian/Fq/?g=3&jacobian=no&polarizable=not_no", "3.2.a_a_ae")
self.check_args("/Variety/Abelian/Fq/?g=3&jacobian=no&polarizable=yes", "3.2.a_ac_a")
+
+ def test_search_cyclic_group(self):
+ r"""
+ Check that we can restrict to cyclic or non-cyclic groups of points
+ using the cyclic search parameter.
+ """
+ # 1.2.ac has cyclic group of points (is_cyclic = True)
+ self.check_args(
+ "/Variety/Abelian/Fq/?cyclic=yes",
+ ">1.2.ac<",
+ )
+
+ # 1.3.a has non-cyclic group of points (is_cyclic = False)
+ self.check_args(
+ "/Variety/Abelian/Fq/?cyclic=no",
+ ">1.3.a<",
+ )
+
+ # And make sure they do not appear in the wrong list
+ self.not_check_args(
+ "/Variety/Abelian/Fq/?cyclic=yes",
+ ">1.3.a<",
+ )
+ self.not_check_args(
+ "/Variety/Abelian/Fq/?cyclic=no",
+ ">1.2.ac<",
+ )
+
+ def test_search_noncyclic_primes(self):
+ r"""
+ Check that the noncyclic_primes search parameter is accepted and
+ finds classes that are non-cyclic at p = 2.
+ """
+ self.check_args(
+ "/Variety/Abelian/Fq/?noncyclic_primes=2",
+ ">1.3.a<",
+ )
diff --git a/lmfdb/ecnf/code.yaml b/lmfdb/ecnf/code.yaml
index fc30514363..40a76c23fc 100644
--- a/lmfdb/ecnf/code.yaml
+++ b/lmfdb/ecnf/code.yaml
@@ -46,7 +46,7 @@ cond:
cond_norm:
comment: Compute the norm of the conductor
sage: E.conductor().norm()
- pari: idealnorm(ellglobalred(E)[1])
+ pari: idealnorm(K, ellglobalred(E)[1])
magma: Norm(Conductor(E));
disc:
@@ -128,4 +128,6 @@ snippet_test:
label: 81.1-CMa1
langs:
url: EllipticCurve/2.0.3.1/81.1/CMa/1/download/{lang}
+
+
diff --git a/lmfdb/elliptic_curves/code.yaml b/lmfdb/elliptic_curves/code.yaml
index 663cbf6b3f..d733d6296a 100644
--- a/lmfdb/elliptic_curves/code.yaml
+++ b/lmfdb/elliptic_curves/code.yaml
@@ -180,10 +180,6 @@ qexp:
comment: q-expansion of modular form
sage: E.q_eigenform(20)
pari: |
- \\ actual modular form, use for small N
- [mf,F] = mffromell(E)
- Ser(mfcoefs(mf,20),q)
- \\ or just the series
Ser(ellan(E,20),q)*q
magma: ModularForm(E);
@@ -251,4 +247,4 @@ snippet_test:
test37a1:
label: 37.a1
url: EllipticCurve/Q/37/a/1/download/{lang}?label=37.a1
-
+
diff --git a/lmfdb/groups/abstract/templates/abstract-show-subgroup.html b/lmfdb/groups/abstract/templates/abstract-show-subgroup.html
index 72bc662d26..ccee8edb21 100644
--- a/lmfdb/groups/abstract/templates/abstract-show-subgroup.html
+++ b/lmfdb/groups/abstract/templates/abstract-show-subgroup.html
@@ -160,11 +160,7 @@ Automorphism information
| $\operatorname{Aut}(G)$ | {{seq.amb.show_aut_group()|safe}} |
| $\operatorname{Aut}(H)$ |
- {% if seq.sub|attr('show_aut_group') %}
{{seq.sub.show_aut_group()|safe}}
- {% else %}
- not computed
- {% endif %}
|
{% if seq.aut_weyl_group is not none %}
| $\operatorname{res}({{S}})$ | ${{seq.aut_weyl.tex_name}}$, of order {{info.pos_int_and_factor(seq.aut_weyl.order) | safe}} |
diff --git a/lmfdb/groups/abstract/web_groups.py b/lmfdb/groups/abstract/web_groups.py
index b7cc4880b7..94ebb57197 100644
--- a/lmfdb/groups/abstract/web_groups.py
+++ b/lmfdb/groups/abstract/web_groups.py
@@ -2572,7 +2572,7 @@ def show_aut_group(self):
if self.aut_order is None:
return "not computed"
aut_order = pos_int_and_factor(self.aut_order)
- tex = self.aut_tex
+ tex = getattr(self, "aut_tex", None)
if tex is None:
return f'Group of order {aut_order}'
else:
@@ -2580,10 +2580,7 @@ def show_aut_group(self):
if self.aut_group is not None:
url = url_for(".by_label", label=self.aut_group)
return f'${tex}$, of order {aut_order}'
- if tex is None:
- return f"Group of order {aut_order}"
- else:
- return f'${tex}$'
+ return f"${tex}$, of order {aut_order}"
def aut_group_knowl(self):
if self.aut_order is None:
@@ -2591,7 +2588,7 @@ def aut_group_knowl(self):
if self.live():
return f"Group of order {self.aut_order}"
aut_order = pos_int_and_factor(self.aut_order)
- tex = self.aut_tex
+ tex = getattr(self, "aut_tex", None)
if tex is None:
tex = "Group"
knowl = f'{tex}'
diff --git a/lmfdb/higher_genus_w_automorphisms/main.py b/lmfdb/higher_genus_w_automorphisms/main.py
index 822825bfe0..1b788584fa 100644
--- a/lmfdb/higher_genus_w_automorphisms/main.py
+++ b/lmfdb/higher_genus_w_automorphisms/main.py
@@ -1276,6 +1276,12 @@ class HGCWASearchArray(SearchArray):
jump_egspan = "e.g. 2.12-4.0.2-2-2-3 or 3.168-42.0.2-3-7.2"
jump_knowl = "curve.highergenus.aut.search_input"
jump_prompt = "Label"
+ null_column_explanations = { # No need to display warnings for these
+ 'hyperelliptic': False,
+ 'cyclic_trigonal': False,
+ 'full_label': False,
+ 'full_auto': False,
+ }
def __init__(self):
genus = TextBox(
diff --git a/lmfdb/lmfdb_database.py b/lmfdb/lmfdb_database.py
index 1322310b4c..022a0a2c3b 100644
--- a/lmfdb/lmfdb_database.py
+++ b/lmfdb/lmfdb_database.py
@@ -2,6 +2,7 @@
import os
import shutil
import signal
+import socket
import subprocess
from collections import Counter
from psycopg2.sql import SQL
@@ -317,7 +318,7 @@ def _check_locks(self, changetype, datafile=None, suffix=""):
super()._check_locks(changetype, datafile=datafile, suffix=suffix)
if self._db.config.postgresql_options["user"] != "editor":
raise RuntimeError("You must be logged in as editor to make data changes")
- if changetype in _nolog_changetypes:
+ if changetype in _nolog_changetypes or socket.gethostname() == "proddb":
return
# The following is the definition of run_diskfree used below to find the available space on grace
@@ -380,8 +381,9 @@ def grace_space_available():
tablespace = self._get_tablespace()
cur_size = self._db.table_sizes()[self.search_table]
size_guess = cur_size["total_bytes"]
- if datafile is None and changetype != "create_table_like":
- size_guess = 0 # insert_many, update, upsert. We rely on the 100GB offset to provide enough space since there is no datafile to use
+ if datafile is None:
+ if changetype != "create_table_like":
+ size_guess = 0 # insert_many, update, upsert. We rely on the 100GB offset to provide enough space since there is no datafile to use
else:
size_guess = max(size_guess, os.path.getsize(datafile))
@@ -510,8 +512,9 @@ def log_db_change(self, operation, tablename=None, logid=None, aborted=False, **
- ``**data`` -- any additional information to install in the logging table (will be stored as a json dictionary)
"""
if aborted:
- deleter = SQL("DELETE FROM userdb.ongoing_operations WHERE logid = %s")
- self._execute(deleter, [logid])
+ if socket.gethostname() != "proddb":
+ deleter = SQL("DELETE FROM userdb.ongoing_operations WHERE logid = %s")
+ self._execute(deleter, [logid])
else:
from lmfdb.utils.datetime_utils import utc_now_naive
uid = self.login()
@@ -521,7 +524,7 @@ def log_db_change(self, operation, tablename=None, logid=None, aborted=False, **
"VALUES (%s, %s, %s, %s, %s, %s)"
)
self._execute(inserter, [uid, utc_now_naive(), tablename, logid, operation, data])
- if operation not in _nolog_changetypes:
+ if operation not in _nolog_changetypes and socket.gethostname() != "proddb":
# This table is used by _check_locks to determine if there is enough space to execute an upload
inserter = SQL(
"INSERT INTO userdb.ongoing_operations (logid, finishing, time, tablename, operation, username) "
diff --git a/lmfdb/number_fields/code.yaml b/lmfdb/number_fields/code.yaml
index 739461e07c..b1c6dd243c 100644
--- a/lmfdb/number_fields/code.yaml
+++ b/lmfdb/number_fields/code.yaml
@@ -65,10 +65,13 @@ discriminant:
oscar: OK = ring_of_integers(K); discriminant(OK)
rd:
+ comment: Root discriminant
sage: (K.disc().abs())^(1./K.degree())
pari: abs(K.disc)^(1/poldegree(K.pol))
magma: Abs(Discriminant(OK))^(1/Degree(K));
- oscar: (1.0 * dK)^(1/degree(K))
+ oscar: |
+ OK = ring_of_integers(K);
+ (1.0 * abs(discriminant(OK)))^(1/degree(K))
automorphisms:
comment: Autmorphisms
@@ -81,7 +84,7 @@ ramified_primes:
sage: K.disc().support()
pari: factor(abs(K.disc))[,1]~
magma: PrimeDivisors(Discriminant(OK));
- oscar: prime_divisors(discriminant((OK)))
+ oscar: prime_divisors(discriminant(OK))
integral_basis:
comment: Integral basis
@@ -161,7 +164,7 @@ class_number_formula:
2^r1 * (2*Pi(RR))^r2 * RK * hK / (wK * Sqrt(RR!Abs(DK)));
oscar: |
# self-contained Oscar code snippet to compute the analytic class number formula
- Qx, x = PolynomialRing(QQ); K, a = NumberField(%s);
+ Qx, x = polynomial_ring(QQ); K, a = number_field(%s);
OK = ring_of_integers(K); DK = discriminant(OK);
UK, fUK = unit_group(OK); clK, fclK = class_group(OK);
r1,r2 = signature(K); RK = regulator(K); RR = parent(RK);
@@ -173,12 +176,15 @@ galois_group:
sage: K.galois_group(type='pari')
pari: polgalois(K.pol)
magma: G = GaloisGroup(K);
- oscar: G, Gtx = galois_group(K); G, transitive_group_identification(G)
+ oscar: |
+ G, Gtx = galois_group(K);
+ degree(K) > 1 ? (G, transitive_group_identification(G)) : (G, nothing)
+
intermediate_fields:
comment: Intermediate fields
sage: K.subfields()[1:-1]
- pari: L = nfsubfields(K); L[2..length(b)]
+ pari: L = nfsubfields(K); L[2..length(L)]
magma: L := Subfields(K); L[2..#L];
oscar: subfields(K)[2:end-1]
@@ -216,3 +222,5 @@ snippet_test:
- oscar
- gp
url: NumberField/2.0.4.1/download/{lang}
+
+
diff --git a/lmfdb/number_fields/number_field.py b/lmfdb/number_fields/number_field.py
index dd302a9580..5a4ce9b914 100644
--- a/lmfdb/number_fields/number_field.py
+++ b/lmfdb/number_fields/number_field.py
@@ -167,6 +167,17 @@ def reliability():
return render_template("single.html", kid='rcs.rigor.nf',
title=t, bread=bread, learnmore=learnmore)
+@nf_page.route("/NumberFieldPictures")
+def nf_picture_page():
+ t = r'Pictures for number fields'
+ bread = bread_prefix() + [('Number Field Picture', ' ')]
+ return render_template(
+ "single.html",
+ kid='nf.picture',
+ title=t,
+ bread=bread,
+ learnmore=learnmore_list())
+
@nf_page.route("/GaloisGroups")
def render_groups_page():
@@ -676,7 +687,8 @@ def render_field_webpage(args):
resinfo.append(('ae', dnc, len(arith_equiv[1])))
info['resinfo'] = resinfo
- learnmore = learnmore_list()
+ learnmore = learnmore_list() + [('Number field pictures', url_for(".nf_picture_page"))]
+
title = "Number field %s" % info['label']
if npr == 1:
@@ -903,6 +915,7 @@ def number_field_search(info, query):
parse_bracketed_posints(info,query,'signature',qfield=('degree','r2'),exactlength=2, allow0=True, extractor=lambda L: (L[0]+2*L[1],L[1]))
parse_signed_ints(info,query,'discriminant',qfield=('disc_sign','disc_abs'))
parse_floats(info, query, 'rd')
+ parse_floats(info, query, 'grd')
parse_floats(info, query, 'regulator')
parse_posints(info,query,'class_number')
parse_posints(info,query,'relative_class_number')
diff --git a/lmfdb/templates/index-boxes.html b/lmfdb/templates/index-boxes.html
index 1db964fa83..5ccd29bbab 100644
--- a/lmfdb/templates/index-boxes.html
+++ b/lmfdb/templates/index-boxes.html
@@ -5,23 +5,18 @@
{{ KNOWL_INC('users.' + userid|lower + '.note') }}
-
-
- {% for box in boxes %}
-
-
-
-  |
- {{ box.title }} |
-
-
- | {{ box.content|safe }} |
-
-
- |
- {% if loop.index is even -%}
{%- endif %}
- {% endfor %}
-
-
+
+ {% for box in boxes %}
+
+
+
+

+
+
+
{{ box.title }}
+
{{ box.content|safe }}
+
+ {% endfor %}
+
{% endblock %}
diff --git a/lmfdb/templates/style.css b/lmfdb/templates/style.css
index 07f1aa9bd5..58af5a4c02 100644
--- a/lmfdb/templates/style.css
+++ b/lmfdb/templates/style.css
@@ -289,6 +289,11 @@ body.knowl #header {
background: {{color.flashes_background}};
}
+#flashes p.success {
+ border-left: 15px solid #43664a;
+ background: #e3fde8;
+}
+
#main {
margin: 0px 0px 0px 184px;
padding: 0px 0px 0px 10px;
@@ -1800,79 +1805,117 @@ body.beta #sidebar table.short:nth-of-type(3) .rotation p.rotation {
-/*Table boxes front page*/
+/* Boxes on front page*/
-table.boxes {
- table-layout: fixed;
- width: 100%; padding: 10;
- margin: 0px;
- margin-top: 35px;
- margin-left: 18px;
- font-size:1em;
- display:table;
- }
-table.boxes > tbody > tr > td {
- padding: 0px 35px 35px 0px;
- overflow: hidden;
- vertical-align: top
- }
-table.box {
- background: {{color.box_background}};
- width: 100%; height: 100%;
- min-height: 175px;
- padding: 10;
- border-radius: 0px;
- font-size:1em;
- }
-table.box td.img {
- background: {{color.box_background_img}};
- width: 140px;
- height: 140px;
- border: 15;
- padding-left: 7px;
- padding-bottom: 2px;
- padding-top: 7px;
- padding-right: 7px;
- border-top-left-radius: 10px;
- border-bottom-left-radius: 10px;
- }
-table.box img {
- border: 15;
- padding: 5;
- }
-table td.title {
- font-weight: bold;
- font-size: 110%;
- background: {{color.box_background_title}};
- height: 2em;
- padding: 5;
- padding-left: 10px;
- border-top-right-radius: 10px;
- }
-table td.content {
- background: {{color.box_background_img}};
- font-size: 90%;
- padding-left: 10px;
- padding-bottom: 2px;
- vertical-align: top;
- border-bottom-right-radius: 10px;
+
+.box-container {
+ display: grid;
+
+ gap: 2.5vw 2.5vw;
+
+ justify-content: space-evenly;
+
+ align-items: top;
+ grid-template-columns: 1fr 1fr;
+
+ margin: 20px;
+
+}
+
+/* small screen layout; 720p or mobile */
+@media (max-width: 1000px) {
+ .box-container {
+ grid-template-columns: 1fr;
}
-table td.content ul li {
- padding: 3px 0px;
+}
+
+/* 1440p monitor, */
+@media (min-width: 2550px) {
+ .box-container {
+ grid-template-columns: 1fr 1fr 1fr 1fr;
}
+}
+
+.parent-box {
+ background: {{color.box_background}};
+
+ line-height: 135%;
+ text-align: center;
+
+ display: grid;
+ grid-template-rows: 2.5em 4fr;
+ grid-template-columns: 1fr 3fr;
+ margin: 0;
+ padding: 0;
+
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.img-box {
+ grid-row: 1 / span 2;
+ grid-column: 1;
+
+ background: {{color.box_background_img}};
+
+ /* to center image: */
+ display: flex;
+ justify-content: center;
+}
+
+.img-container {
+ padding: 10px 10px;
+
+ display: flex;
+ justify-content: center;
+
+ width: 100%;
+ height: auto;
+ overflow: hidden;
+ max-width: 150px;
+ min-width: 150px;
+}
+
+.img-container img {
+ width: 100%;
+ height: auto;
+ object-fit: contain;
+ display: block;
+}
+
+div.title {
+ grid-row: 1;
+ grid-column: 2;
+ padding-left: 10px;
+ font-weight: bold;
+ font-size: 110%;
+ background: {{color.box_background_title}};
+ text-align: left;
+
+ display: grid;
+ align-items: center;
+}
+
+div.content {
+ grid-row: 2;
+ grid-column: 2;
+
+ background: {{color.box_background_img}};
+
+ font-size: 90%;
+ text-align: left;
+ padding-left: 8px;
+ padding-right: 8px;
+}
+
+
div.user {
- font-size: 90%;
- padding: 0px;
- }
+ font-size: 90%;
+ padding: 0px;
+}
+
div.user ul li {
- line-height: 90%;
- }
-table.box td.content ul.list {
- margin: 0px;
- padding-left: 15px; padding-bottom: 5px;
- list-style-type: none;
- list-style-position: inside;
- line-height: 135%;
+ line-height: 90%;
}
/*End table boxes front page*/
diff --git a/lmfdb/tests/snippet_tests/ecnf/code-11.1-a1-gp.log b/lmfdb/tests/snippet_tests/ecnf/code-11.1-a1-gp.log
index c5e681287a..19defd2d3b 100644
--- a/lmfdb/tests/snippet_tests/ecnf/code-11.1-a1-gp.log
+++ b/lmfdb/tests/snippet_tests/ecnf/code-11.1-a1-gp.log
@@ -7,9 +7,8 @@ gp> ellglobalred(E)[1]
[ 0 1]
-gp> idealnorm(ellglobalred(E)[1])
- *** too few arguments: ...alnorm(ellglobalred(E)[1])
- *** ^-
+gp> idealnorm(K, ellglobalred(E)[1])
+11
gp> E.disc
Mod(-11, x^2 - x + 3)
gp> norm(E.disc)
diff --git a/lmfdb/tests/snippet_tests/ecnf/code-81.1-CMa1-gp.log b/lmfdb/tests/snippet_tests/ecnf/code-81.1-CMa1-gp.log
index 4fcba5d94b..fae66bdb2c 100644
--- a/lmfdb/tests/snippet_tests/ecnf/code-81.1-CMa1-gp.log
+++ b/lmfdb/tests/snippet_tests/ecnf/code-81.1-CMa1-gp.log
@@ -7,9 +7,8 @@ gp> ellglobalred(E)[1]
[0 9]
-gp> idealnorm(ellglobalred(E)[1])
- *** too few arguments: ...alnorm(ellglobalred(E)[1])
- *** ^-
+gp> idealnorm(K, ellglobalred(E)[1])
+81
gp> E.disc
Mod(-27, x^2 - x + 1)
gp> norm(E.disc)
diff --git a/lmfdb/tests/snippet_tests/elliptic_curves/code-11.a3-gp.log b/lmfdb/tests/snippet_tests/elliptic_curves/code-11.a3-gp.log
index ad2d9d5f12..859ff2a27a 100644
--- a/lmfdb/tests/snippet_tests/elliptic_curves/code-11.a3-gp.log
+++ b/lmfdb/tests/snippet_tests/elliptic_curves/code-11.a3-gp.log
@@ -29,12 +29,6 @@ gp> elltors(E)[1]
5
gp> [r,L1r] = ellanalyticrank(E); L1r/r!
0.25384186085591068433775892335090946104
-gp> [mf,F] = mffromell(E)
-[[[11, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], 0], [], [[[Vecsmall([9]), [11, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1]]], Vecsmall([1]), [Vecsmall([2]), [Mat(1), 1, 1, 0], [0; 1]], [0, 0, 0, 0, 0]], [[Vecsmall([5]), [11, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], [0, -1, 1, 0, 0, -4, 0, 1, -1, 16, -152, -11, -4096/11, Vecsmall([1]), [Vecsmall([128, -1])], [[6.3460465213977671084439730837727365261, 3.1730232606988835542219865418863682630 - 1.4588166169384952293308896129036752572*I], 0, [-0.41964337760708056627592628232664330021, 0.70982168880354028313796314116332165011 + 0.30314536460359968462967109851401150148*I, 0.70982168880354028313796314116332165011 - 0.30314536460359968462967109851401150148*I, 0.60629072920719936925934219702802300295*I, -1.1294650664106208494138894234899649503 + 0.30314536460359968462967109851401150148*I, -1.1294650664106208494138894234899649503 - 0.30314536460359968462967109851401150148*I]~, 0, [], [11, 1, Mat([11, 1]), [[1, 5, 0, 1]]], [1, Vecsmall([-1])], [[]~]]]], [1]~]
-gp> Ser(mfcoefs(mf,20),q)
- *** at top-level: Ser(mfcoefs(mf,20),q)
- *** ^---------------------
- *** Ser: incorrect type in Ser (t_MAT).
gp> Ser(ellan(E,20),q)*q
q - 2*q^2 - q^3 + 2*q^4 + q^5 + 2*q^6 - 2*q^7 - 2*q^9 - 2*q^10 + q^11 - 2*q^12 + 4*q^13 + 4*q^14 - q^15 - 4*q^16 - 2*q^17 + 4*q^18 + 2*q^20 + O(q^21)
gp> ellmoddegree(E)
diff --git a/lmfdb/tests/snippet_tests/elliptic_curves/code-37.a1-gp.log b/lmfdb/tests/snippet_tests/elliptic_curves/code-37.a1-gp.log
index eef8fa2c19..4784a1ddc0 100644
--- a/lmfdb/tests/snippet_tests/elliptic_curves/code-37.a1-gp.log
+++ b/lmfdb/tests/snippet_tests/elliptic_curves/code-37.a1-gp.log
@@ -29,12 +29,6 @@ gp> elltors(E)[1]
1
gp> [r,L1r] = ellanalyticrank(E); L1r/r!
0.30599977383405230182048368332167647445
-gp> [mf,F] = mffromell(E)
-[[[37, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], 0], [], [[[Vecsmall([9]), [37, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1]], [[Vecsmall([20]), [37, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], Vecsmall([2, 2, 37]), [[Vecsmall([9]), [37, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1]]]], Vecsmall([1, 2]), [Vecsmall([2, 3]), [[2, 1; 1, 1], 2, 1, 0], [0, 0; 2, -2; -2, 4]], [0, 0, 0, 0, 0]], [[Vecsmall([5]), [37, 2, [[[1, [0]], [1, [], []], [[]~, Vecsmall([])], [[], [], [;], [], [], []], [;]], []~, 1, t - 1], y]], [0, 0, 1, -1, 0, 0, -2, 1, -1, 48, -216, 37, 110592/37, Vecsmall([1]), [Vecsmall([128, 1])], [[2.9934586462319596298320099794525081778, -2.4513893819867900608542248318665252253*I], 0, [0.83756543528332303544481089907503024040, 0.26959443640544455826293795134926000405, -1.1071598716887675937077488504242902445, 1.3767543080942121519706868017735502485, 1.9447253069720906291525597494993204849, 0.56797099887787847718187294772577023635]~, [-1.1536613682079794870404572293106210406, -1.0509745834909744209136981100086967207], [[0, 0]], [37, 1, Mat([37, 1]), [[1, 5, 0, 1]]], [-1, Vecsmall([1])], [[]~]]]], [0, -1/2]~]
-gp> Ser(mfcoefs(mf,20),q)
- *** at top-level: Ser(mfcoefs(mf,20),q)
- *** ^---------------------
- *** Ser: incorrect type in Ser (t_MAT).
gp> Ser(ellan(E,20),q)*q
q - 2*q^2 - 3*q^3 + 2*q^4 - 2*q^5 + 6*q^6 - q^7 + 6*q^9 + 4*q^10 - 5*q^11 - 6*q^12 - 2*q^13 + 2*q^14 + 6*q^15 - 4*q^16 - 12*q^18 - 4*q^20 + O(q^21)
gp> ellmoddegree(E)
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-gp.log b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-gp.log
index 492df13efa..46e96fe4eb 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-gp.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-gp.log
@@ -26,10 +26,8 @@ gp> K.reg
gp> K = bnfinit(x, 1);
gp> [polcoeff (lfunrootres (lfuncreate (K))[1][1][2], -1), 2^K.r1 * (2*Pi)^K.r2 * K.reg * K.no / (K.tu[1] * sqrt (abs (K.disc)))]
[1.0000000000000000000000000000000000000, 1.0000000000000000000000000000000000000]
-gp> L = nfsubfields(K); L[2..length(b)]
- *** at top-level: L=nfsubfields(K);L[2..length(b)]
- *** ^---------------
- *** _[_.._]: inconsistent dimensions in _[..].
+gp> L = nfsubfields(K); L[2..length(L)]
+[]
gp> polgalois(K.pol)
[1, 1, 1, "S1"]
gp> p = 7; pfac = idealprimedec(K, p); vector(length(pfac), j, [pfac[j][3], pfac[j][4]])
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-oscar.log b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-oscar.log
index 14c61015cb..70ef048b7a 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-oscar.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-oscar.log
@@ -15,7 +15,7 @@ julia> signature(K)
julia> OK = ring_of_integers(K); discriminant(OK)
1
-julia> prime_divisors(discriminant((OK)))
+julia> prime_divisors(discriminant(OK))
ZZRingElem[]
julia> automorphisms(K)
@@ -49,12 +49,7 @@ julia> [K(fUK(a)) for a in gens(UK)]
julia> regulator(K)
1.0000
-julia> Qx, x = PolynomialRing(QQ); K, a = NumberField(x);
-ERROR: UndefVarError: `PolynomialRing` not defined in `Main`
-Suggestion: check for spelling errors or missing imports.
-Stacktrace:
- [1] top-level scope
- @ none:1
+julia> Qx, x = polynomial_ring(QQ); K, a = number_field(x);
julia> OK = ring_of_integers(K); DK = discriminant(OK);
@@ -70,19 +65,10 @@ julia> 2^r1 * (2*pi)^r2 * RK * hK / (wK * sqrt(RR(abs(DK))))
julia> subfields(K)[2:end-1]
Tuple{AbsSimpleNumField, NumFieldHom{AbsSimpleNumField, AbsSimpleNumField, Hecke.MapDataFromAnticNumberField{AbsSimpleNumFieldElem}, Hecke.MapDataFromAnticNumberField{AbsSimpleNumFieldElem}, AbsSimpleNumFieldElem}}[]
-julia> G, Gtx = galois_group(K); G, transitive_group_identification(G)
-ERROR: ArgumentError: degree must be positive, not 0
-Stacktrace:
- [1] macro expansion
- @ ~/.julia/packages/AbstractAlgebra/T6WZD/src/Assertions.jl:602 [inlined]
- [2] has_transitive_groups(deg::Int64)
- @ Oscar ~/.julia/packages/Oscar/6oHzG/src/Groups/libraries/transitivegroups.jl:52
- [3] macro expansion
- @ ~/.julia/packages/AbstractAlgebra/T6WZD/src/Assertions.jl:601 [inlined]
- [4] transitive_group_identification(G::PermGroup)
- @ Oscar ~/.julia/packages/Oscar/6oHzG/src/Groups/libraries/transitivegroups.jl:149
- [5] top-level scope
- @ none:1
+julia> G, Gtx = galois_group(K);
+
+julia> degree(K) > 1 ? (G, transitive_group_identification(G)) : (G, nothing)
+(Permutation group of degree 1 and order 1, nothing)
julia> p = 7; pfac = factor(ideal(ring_of_integers(K), p)); [(e, valuation(norm(pr),p)) for (pr,e) in pfac]
1-element Vector{Tuple{Int64, Int64}}:
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-sage.log b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-sage.log
index d7eb84890c..3b31a20fb6 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-sage.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-1.1.1.1-sage.log
@@ -38,7 +38,7 @@ sage: K.subfields()[1:-1]
]
sage: K.galois_group(type='pari')
-:1: DeprecationWarning: the different Galois types have been merged into one class
+:1: DeprecationWarning: the different Galois types have been merged into one class
See https://github.com/sagemath/sage/issues/28782 for details.
K.galois_group(type='pari')
Galois group 1T1 (S1) with order 1 of x
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-gp.log b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-gp.log
index f008eabd62..ddad40acef 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-gp.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-gp.log
@@ -26,7 +26,7 @@ gp> K.reg
gp> K = bnfinit(x^2 + 1, 1);
gp> [polcoeff (lfunrootres (lfuncreate (K))[1][1][2], -1), 2^K.r1 * (2*Pi)^K.r2 * K.reg * K.no / (K.tu[1] * sqrt (abs (K.disc)))]
[0.78539816339744830961566084581987572105, 0.78539816339744830961566084581987572105]
-gp> L = nfsubfields(K); L[2..length(b)]
+gp> L = nfsubfields(K); L[2..length(L)]
[[x^2 + 1, x]]
gp> polgalois(K.pol)
[2, -1, 1, "S2"]
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-oscar.log b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-oscar.log
index c3cf2fb7b2..0a1b0531c2 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-oscar.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-oscar.log
@@ -15,7 +15,7 @@ julia> signature(K)
julia> OK = ring_of_integers(K); discriminant(OK)
-4
-julia> prime_divisors(discriminant((OK)))
+julia> prime_divisors(discriminant(OK))
1-element Vector{ZZRingElem}:
2
@@ -51,12 +51,7 @@ julia> [K(fUK(a)) for a in gens(UK)]
julia> regulator(K)
1.0000
-julia> Qx, x = PolynomialRing(QQ); K, a = NumberField(x^2 + 1);
-ERROR: UndefVarError: `PolynomialRing` not defined in `Main`
-Suggestion: check for spelling errors or missing imports.
-Stacktrace:
- [1] top-level scope
- @ none:1
+julia> Qx, x = polynomial_ring(QQ); K, a = number_field(x^2 + 1);
julia> OK = ring_of_integers(K); DK = discriminant(OK);
@@ -72,7 +67,9 @@ julia> 2^r1 * (2*pi)^r2 * RK * hK / (wK * sqrt(RR(abs(DK))))
julia> subfields(K)[2:end-1]
Tuple{AbsSimpleNumField, NumFieldHom{AbsSimpleNumField, AbsSimpleNumField, Hecke.MapDataFromAnticNumberField{AbsSimpleNumFieldElem}, Hecke.MapDataFromAnticNumberField{AbsSimpleNumFieldElem}, AbsSimpleNumFieldElem}}[]
-julia> G, Gtx = galois_group(K); G, transitive_group_identification(G)
+julia> G, Gtx = galois_group(K);
+
+julia> degree(K) > 1 ? (G, transitive_group_identification(G)) : (G, nothing)
(Symmetric group of degree 2, (2, 1))
julia> p = 7; pfac = factor(ideal(ring_of_integers(K), p)); [(e, valuation(norm(pr),p)) for (pr,e) in pfac]
diff --git a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-sage.log b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-sage.log
index 8c8447cf28..0b1aa5b596 100644
--- a/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-sage.log
+++ b/lmfdb/tests/snippet_tests/number_fields/code-2.0.4.1-sage.log
@@ -40,7 +40,7 @@ sage: K.subfields()[1:-1]
]
sage: K.galois_group(type='pari')
-:1: DeprecationWarning: the different Galois types have been merged into one class
+:1: DeprecationWarning: the different Galois types have been merged into one class
See https://github.com/sagemath/sage/issues/28782 for details.
K.galois_group(type='pari')
Galois group 2T1 (S2) with order 2 of x^2 + 1
diff --git a/lmfdb/tests/test_utils.py b/lmfdb/tests/test_utils.py
index 0c05f6c5a5..96eebd7444 100644
--- a/lmfdb/tests/test_utils.py
+++ b/lmfdb/tests/test_utils.py
@@ -27,9 +27,16 @@
web_latex_split_on_pm,
web_latex_split_on_re,
web_latex_ideal_fact,
- list_to_latex_matrix
+ list_to_latex_matrix,
)
+from lmfdb.utils.completeness import (
+ results_complete,
+ IntegerSet,
+ top,
+ bottom,
+ infinity,
+)
class UtilsTest(unittest.TestCase):
"""
@@ -199,3 +206,198 @@ def test_list_to_latex_matrix(self):
malformed = [[1,0], [0]]
malform_rep = '\\left(\\begin{array}{rr}1 & 0\\\\0\\end{array}\\right)'
self.assertEqual(list_to_latex_matrix(malformed), malform_rep)
+
+ def test_integer_set(self):
+ A = IntegerSet([2, 4])
+ B = IntegerSet([6, 9])
+ C = IntegerSet([-11, 5])
+ self.assertEqual(str(A + A), "[4, 8]")
+ self.assertEqual(str(A - A), "[-2, 2]")
+ self.assertEqual(str(A * A), "[4, 16]")
+ self.assertEqual(str(A / A), "[1, 2]")
+ self.assertEqual(str(-A), "[-4, -2]")
+ self.assertEqual(str(~A), "[1/4, 1/2]")
+ self.assertEqual(str(A + B), "[8, 13]")
+ self.assertEqual(str(A - B), "[-7, -2]")
+ self.assertEqual(str(A * B), "[12, 36]")
+ self.assertEqual(str(A / B), "{}")
+ self.assertEqual(str(A + C), "[-9, 9]")
+ self.assertEqual(str(A - C), "[-3, 15]")
+ self.assertEqual(str(A * C), "[-44, 20]")
+ self.assertEqual(str(A / C), "[-4, -1] ∪ [1, 4]")
+ self.assertEqual(str(B + A), "[8, 13]")
+ self.assertEqual(str(B - A), "[2, 7]")
+ self.assertEqual(str(B * A), "[12, 36]")
+ self.assertEqual(str(B / A), "[2, 4]")
+ self.assertEqual(str(B + B), "[12, 18]")
+ self.assertEqual(str(B - B), "[-3, 3]")
+ self.assertEqual(str(B * B), "[36, 81]")
+ self.assertEqual(str(B / B), "{1}")
+ self.assertEqual(str(-B), "[-9, -6]")
+ self.assertEqual(str(~B), "[1/9, 1/6]")
+ self.assertEqual(str(B + C), "[-5, 14]")
+ self.assertEqual(str(B - C), "[1, 20]")
+ self.assertEqual(str(B * C), "[-99, 45]")
+ self.assertEqual(str(B / C), "[-9, -1] ∪ [2, 9]")
+ self.assertEqual(str(C + A), "[-9, 9]")
+ self.assertEqual(str(C - A), "[-15, 3]")
+ self.assertEqual(str(C * A), "[-44, 20]")
+ self.assertEqual(str(C / A), "[-5, 2]")
+ self.assertEqual(str(C + B), "[-5, 14]")
+ self.assertEqual(str(C - B), "[-20, -1]")
+ self.assertEqual(str(C * B), "[-99, 45]")
+ self.assertEqual(str(C / B), "[-1, 0]")
+ self.assertEqual(str(C + C), "[-22, 10]")
+ self.assertEqual(str(C - C), "[-16, 16]")
+ self.assertEqual(str(C * C), "[-55, 121]")
+ self.assertEqual(str(C / C), "[-11, 11]")
+ self.assertEqual(str(-C), "[-5, 11]")
+ self.assertEqual(str(~C), "(-oo, -1/11] ∪ [1/5, +oo)")
+ self.assertEqual(str(abs(C)), "[0, 11]")
+
+ self.assertEqual(str(B.pow_cap(A, 1.5)), "[6, 8]")
+ self.assertEqual(str(A.union(B, C)), "[-11, 9]")
+ self.assertEqual(str(C.intersection(A * A)), "[4, 5]")
+ self.assertEqual(str(C.difference(-A,A)), "[-11, -5] ∪ [-1, 1] ∪ {5}")
+ self.assertEqual(A.is_subset(C), True)
+ self.assertEqual(B.is_subset(A + A), False)
+ self.assertEqual(A <= B, True)
+ self.assertEqual(A <= A + A, True)
+ self.assertEqual(C <= A + A, False)
+ self.assertEqual(A < B, True)
+ self.assertEqual(A < A + A, False)
+ self.assertEqual(A.bounded(5), True)
+ self.assertEqual(A.bounded(4), True)
+ self.assertEqual(A.bounded(3), False)
+ self.assertEqual(A.bounded(2,4), True)
+ self.assertEqual(A.restricted(), True)
+ self.assertEqual(IntegerSet(None).restricted(), False)
+ self.assertEqual(A.min(), 2)
+ self.assertEqual(A.max(), 4)
+ self.assertEqual(top(4).min(), -infinity)
+ self.assertEqual(top(4).max(), 4)
+
+ self.assertEqual(list(IntegerSet([5, 40]).stickelberger(2, [0])), [(5,), (2,), (2, 3), (13,), (17,), (3, 7), (2, 3), (2, 7), (29,), (3, 11), (37,), (2, 5)])
+ self.assertEqual(list(IntegerSet([3, 40]).stickelberger(2, [1])), [(3,), (2,), (7,), (2,), (11,), (3, 5), (19,), (2, 5), (23,), (2, 3), (31,), (5, 7), (3, 13), (2, 5)])
+ self.assertEqual(list(IntegerSet([200, 220]).stickelberger(3, [0])), [(2, 5), (3, 67), (2, 3, 17), (5, 41), (2, 13), (11, 19), (2, 53), (3, 71), (2, 3), (7, 31), (2, 5, 11)])
+
+ self.assertEqual(A.is_finite(), True)
+ self.assertEqual(IntegerSet(None).is_finite(), False)
+ self.assertEqual(top(4).is_finite(), False)
+ self.assertEqual(bottom(4).is_finite(), False)
+
+ X = [
+ (3, 150000), # 3
+ (4, 100000), # 4
+ ([5,14], 50000), # 7,8,11
+ (15, 1000), # 15
+ ([16,19], 15000), # 19
+ (20, 1000), # 20
+ ([21,23], 3000), # 23
+ (24, 1000), # 24
+ ([25,34], 5000), # 31
+ ([35,40], 1000), # 35,39,40
+ ([41,46], 15000), # 43
+ ([47, 59], 1000), # 47,51,52,55,56,59
+ ([60, 67], 10000), # 67
+ ([68, 120], 1000),
+ ([121, 159], 100),
+ ([160, 163], 5000), # 163
+ ([164, 702], 100), # 703 is the first missing
+ ]
+ self.assertEqual(A.bound_under(X), None)
+ self.assertEqual((A + A).bound_under(X), 50000)
+ self.assertEqual((A * B).bound_under(X), 1000)
+
+ def test_complete(self):
+ from lmfdb import db
+ for tup in [
+ ("lfunc_search", {"rational": True, "degree": 1, "conductor": {"$lte": 100}}, 'L-functions with degree 1 and conductor at most 2800'),
+ ("maass_rigor", {"level": 3, "spectral_parameter": {"$lte": 21}}, 'Maass forms with level 3 and spectral parameter at most 24.9526'),
+ ("mf_newforms", {'level': {'$gte': 4, '$lte': 12}, 'weight': {'$gte': 3, '$lte': 6}}, "newforms with $Nk^2$ at most 4000"),
+ ("mf_newforms", {'level': {'$gte': 24, '$lte': 100}, 'weight': {'$gte': 10, '$lte': 16}, 'char_orbit_index': 1}, "newforms with trivial character and $Nk^2$ at most 40000"),
+ ("mf_newforms", {'level': {'$gte': 12, '$lte': 20}, 'weight': {'$gte': 20, '$lte': 40}}, "newforms with level $N$ at most 24 and $Nk^2$ at most 40000"),
+ ("mf_newforms", {'level': {'$gte': 4, '$lte': 8}, 'weight': {'$gte': 60, '$lte': 100}}, "newforms with level $N$ at most 10 and $Nk^2$ at most 100000"),
+ ("mf_newforms", {'level': {'$gte': 96, '$lte': 100}, 'weight': {'$gte': 8, '$lte': 12}}, "newforms with level at most 100 and weight at most 12"),
+ ("mf_newforms", {'level': {'$gte': 37000, '$lte': 49000}, 'weight': 2, 'prim_orbit_index': 1}, "newforms with trivial character, weight 2, and level at most 50000"),
+ ("mf_newforms", {'level': 900001, 'weight': 2, 'char_order': 1}, "newforms with trivial character, weight 2 and prime level at most a million"),
+ ("hmf_forms", {'deg': 4, 'disc': {'$gte': 1, '$lte': 1200}, 'level_norm': {'$gte': 1, '$lte': 40}}, "Hilbert modular forms over 4.4.725.1, 4.4.1125.1 of level norm at most 991"),
+ ("hmf_forms", {'field_label': '3.3.1929.1', 'level_norm': {'$gte': 1, '$lte': 50}}, "Hilbert modular forms over 3.3.1929.1 of level norm at most 53"),
+ ("bmf_forms", {'field_disc': {'$gte': -12, '$lte': -3}, 'level_norm': {'$gte': 1, '$lte': 40000}}, "Bianchi modular forms with level norm at most 50000 over imaginary quadratic fields with absolute discriminant in [3, 12]"),
+ ("ec_nfcurves", {'field_label': '3.3.1929.1', 'conductor_norm': {'$gte': 1, '$lte': 50}}, "elliptic curves with conductor norm at most 2059 over totally real cubic fields with discriminant 1957"),
+ ("nf_fields", {'disc_abs': {'$gte': 1, '$lte': 10000}}, "number fields with absolute discriminant at most 1656109"),
+ ("nf_fields", {'degree': 3, 'r2': 0, 'disc_abs': {'$gte': 1, '$lte': 2000000}}, "number fields with degree 3, signature [3,0], absolute discriminant at most 3375000"),
+ ("nf_fields", {'degree': 1}, "number fields with degree 1"),
+ ("nf_fields", {'degree': 14, 'r2': 1, 'disc_sign': 1}, "number fields with incompatible conditions: signature and discriminant"),
+ ("nf_fields", {'degree': 2, 'r2': 1, 'class_number': 17}, "number fields with signature [0,1], class number at most 100 (except 98)"),
+ ("nf_fields", {'degree': 2, 'r2': 1, 'class_group': [2, 2, 2, 2, 2, 2, 2]}, "number fields with signature [0,1], class group of exponent 2", "depends on GRH"),
+ ("nf_fields", {'degree': 5, 'rd': {'$gte': 40, '$lte': 60}, 'grd': {'$gte': 20, '$lte': 30}}, "number fields with incompatible conditions: root discriminant and Galois root discriminant"),
+ ("nf_fields", {'degree': 2, 'rd': {'$gte': 30, '$lte': 40}, 'disc_abs': {'$gte': 10000, '$lte': 20000}}, "number fields with absolute discriminant at most 1656109"),
+ ("nf_fields", {'degree': 6, 'r2': 0, 'galois_label': '6T11', 'disc_abs': {'$gte': 1200000000, '$lte': 1800000000}}, "number fields with degree 6, signature [6,0], Galois group 6T11, absolute discriminant at most 1838265625"),
+ ("nf_fields", {'degree': 9, 'r2': 0, 'gal_is_abelian': True, 'disc_abs': {'$gte': 1, '$lte': 1900000000000000}}, "number fields with degree 9, signature [9,0], Galois group 9T(1,2,6,7,17), absolute discriminant at most 1953125000000000"),
+ ("nf_fields", {'degree': 6, 'disc_abs': 489631389843456}, "number fields with degree 6, unramified outside {2,3,7}"),
+ ("nf_fields", {'degree': 5, 'disc_abs': 4130513738895632747}, "number fields with degree 5, unramified outside {107,131}"),
+ ("nf_fields", {'degree': 5, 'disc_abs': 38602932139211521}, "number fields with degree 5, unramified outside {107,131}"),
+ ("nf_fields", {'degree': 7, 'galois_label': '7T1', 'disc_abs': 446132784330195495457232}, "number fields with degree 7, Galois group 7T1, unramified outside {2,7,11,17,37,41}"),
+ ("nf_fields", {'degree': 5, 'galois_label': '5T4', 'disc_abs': 920627786839041}, "number fields with degree 5, Galois group 5T(1,2,4), unramified outside {3,1201}"),
+ ("nf_fields", {'degree': 5, 'galois_label': '5T4', 'gal_is_abelian': True, 'disc_abs': 920627786839041}, "number fields with incompatible conditions: Galois group"),
+ ("nf_fields", {'degree': 5, 'galois_label': '5T4', 'disc_rad': 1254}, "number fields with degree 5, Galois group 5T(1,2,4), unramified outside {2,3,11,19}"),
+ ("nf_fields", {'degree': 8, 'galois_label': '8T25', 'rd': {'$gte': 1, '$lte': 100}}, "number fields with degree 8, Galois group 8T(25,36), Galois root discriminant at most 200"),
+ ("artin_reps", {'GaloisLabel': '6T6', 'Conductor': {'$gte': 1, '$lte': 20000}}, "Artin representations with group 6T6, and conductor at most 22497"),
+ ("gps_groups", {'order': {'$gte': 300, '$lte': 500}}, "groups of order at most 2000 except orders larger than 500 that are multiples of 128"),
+ ("gps_groups", {'perfect': True, 'order': {'$gte': 20000, '$lte': 40000}}, "perfect groups of order at most 50000"),
+ ("gps_groups", {'simple': True, 'abelian': False, 'order': {'$gte': 1, '$lte': 10000000}}, "nonabelian simple groups of order less than 10162031880"),
+ ("gps_groups", {'transitive_degree': {'$gte': 16, '$lte': 24}}, "groups with minimal transitive degree at most 47 (except 32)"),
+ ("gps_groups", {'transitive_degree': 32, 'order': {'$gte': 40000000000, '$lte': 100000000000}}, "groups with minimal transitive degree 32 and order at least 40 billion"),
+ ("gps_groups", {'permutation_degree': {'$gte': 10, '$lte': 14}}, "groups with minimal permutation degree at most 15"),
+ ("gps_groups", {'linQ_dim': 5}, r"groups with linear $\Q$-degree at most 6"),
+ ("ec_curvedata", {'conductor': {'$gte': 300, '$lte': 3000}}, "elliptic curves with conductor at most 500000"),
+ ("ec_curvedata", {'conductor': 1000003}, "elliptic curves with prime conductor at most 300 million"),
+ ("ec_curvedata", {'conductor': 76204800}, "elliptic curves with 7-smooth conductor"),
+ ("ec_curvedata", {'absD': {'$gte': 50000, '$lte': 100000}}, "elliptic curves with minimal discriminant at most 500000"),
+ ("hgcwa_passports", {'genus': 3}, "groups acting as automorphisms of curves of genus 2, 3 or 4"),
+ ("hgcwa_passports", {'genus': 11, 'g0': 0}, "groups G acting as automorphisms of curves X with the genus of X at most 15 and the genus of X/G equal to 0"),
+ ("av_fq_isog", {'g': 1, 'q': 729}, "isogeny classes of elliptic curves over fields of cardinality less than 500 or 512, 625, 729, 1024"),
+ ("av_fq_isog", {'g': {'$gte': 2, '$lte': 4}, 'q': {'$gte': 2, '$lte': 4}}, "isogeny classes of abelian varieties of dimension at most 4 over fields of cardinality at most 5"),
+ ("av_fq_isog", {'q': 3, 'p_rank': 2, 'p_rank_deficit': 2}, "isogeny classes of abelian varieties of dimension at most 4 over fields of cardinality at most 5"),
+ ("belyi_galmaps", {'deg': {'$gte': 2, '$lte': 4}}, "Belyi maps of degree at most 6"),
+ ("lf_fields", {'p': 2, 'n': 16}, "p-adic fields of degree at most 23 and residue characteristic at most 199"),
+ ("lf_fields", {'p': 3, 'e': 9, 'f': 2}, "p-adic fields of degree at most 23 and residue characteristic at most 199"),
+ ("lf_families", {'p': 2, 'e': 4, 'f0': {'$gte': 1, '$lte': 2}, 'e0': 2, 'f': 2}, "families of p-adic extensions with absolute degree at most 47, base degree at most 15 and residue characteristic at most 199"),
+ ("char_dirichlet", {'modulus': {'$gte': 40, '$lte': 100}}, "Dirichlet characters with modulus at most a million"),
+ ("hgm_families", {'degree': {'$gte': 4, '$lte': 6}}, "hypergeometric families with degree at most 7"),
+ ("gps_transitive", {'n': 18, 'solv': 1}, "transitive groups of degree at most 47 (except 32)"),
+ ("gps_transitive", {'n': 32, 'order': 384}, "transitive groups of degree 32 and order at most 511"),
+ ("gps_transitive", {'n': 32, 'order': {'$gte': 40000000000, '$lte': 100000000000}}, "transitive groups of degree at most 47 and order at least 40 billion"),
+ ("gps_st", {'rational': True, 'weight': 1, 'degree': {'$gte': 3, '$lte': 5}}, "rational Sato-Tate groups of weight at most 1 and degree at most 6"),
+ ("gps_st", {'rational': True, 'weight': 0, 'degree': 1}, "rational Sato-Tate groups of weight 0 and degree 1"),
+ ("gps_st", {'weight': 0, 'degree': 1, 'components': {'$gte': 40, '$lte': 50}}, "Sato-Tate groups of weight 0, degree 1 and at most 10000 components"),
+ ]:
+ if len(tup) == 3:
+ tbl, query, reason = tup
+ caveat = None
+ else:
+ tbl, query, reason, caveat = tup
+ self.assertEqual(results_complete(tbl, query, db), (True, reason, caveat))
+
+ for tbl, query in [
+ ("maass_rigor", {"level": {"$gte":2, "$lte": 5}, "spectral_parameter": {"$lte": 21}}),
+ ("mf_newforms", {'level': {'$gte': 100, '$lte': 200}, 'weight': {'$gte': 20, '$lte': 30}}),
+ ("hmf_forms", {'deg': 7, 'disc': {'$gte': 1, '$lte': 1200}, 'level_norm': {'$gte': 1, '$lte': 40}}),
+ ("bmf_forms", {'field_disc': {'$gte': -120, '$lte': -3}, 'level_norm': {'$gte': 1, '$lte': 4000}}),
+ ("ec_nfcurves", {'field_label': '7.7.20134393.1', 'conductor_norm': {'$gte': 1, '$lte': 50}}),
+ ("nf_fields", {'degree': 6, 'disc_abs': {'$gte': 1, '$lte': 20000000}}),
+ ("artin_reps", {'GaloisLabel': '8T34', 'Conductor': {'$gte': 1, '$lte': 200}}),
+ ("gps_groups", {'order': {'$gte': 300, '$lte': 600}}),
+ ("ec_curvedata", {'rank': 6}),
+ ("hgcwa_passports", {'genus': 6}),
+ ("av_fq_isog", {'g': 6, 'q': 3}),
+ ("belyi_galmaps", {'deg': 8}),
+ ("lf_fields", {'p': 2, 'n': 24}),
+ ("lf_families", {'p': 2, 'e': 4, 'f0': {'$gte': 1, '$lte': 4}, 'e0': 2, 'f': 2}),
+ ("char_dirichlet", {'modulus': {'$gte': 400000, '$lte': 3000000}}),
+ ("hgm_families", {'degree': 8}),
+ ("gps_transitive", {'n': 32, 'solv': 1}),
+ ("gps_st", {'rational': True, 'weight': 1, 'degree': 8}),
+ ]:
+ self.assertEqual(results_complete(tbl, query, db)[0], False)
diff --git a/lmfdb/utils/completeness.py b/lmfdb/utils/completeness.py
new file mode 100644
index 0000000000..bb1019dacd
--- /dev/null
+++ b/lmfdb/utils/completeness.py
@@ -0,0 +1,2529 @@
+"""
+This module implements completeness statements for LMFDB queries.
+
+The interface is through the ``results_complete`` function, which takes as input a table name, a query, the LMFDB db object and optionally a search_array object (used to negate null count overrides that mark results as not complete if they refer to a column that may not all be computed). It returns as output a triple: whether the results are complete, a string giving a reason (if so), and a string giving caveats (e.g. any dependence on conjectures; None if no caveats).
+
+EXAMPLES::
+
+ sage: from lmfdb import db
+ sage: from lmfdb.utils.completeness import results_complete
+ sage: results_complete("nf_fields", {"degree": 2, "r2": 1, "class_number": 17}, db)
+ (True, 'number fields with signature [0,1], class number at most 100 (except 98)', None)
+"""
+
+
+from collections import defaultdict
+from sage.all import factor, prod, factorial, is_prime, prime_range, ZZ, NN, ceil, floor, RealSet, infinity, cached_function, RLF
+
+# This dictionary is filled in the __init__ method of CompletenessCheckers based on the table name;
+# specific CompletenessCheckers are created at the bottom of this file.
+lookup = {}
+
+def results_complete(table, query, db, search_array=None):
+ """
+ Determines whether the LMFDB contains all objects satisfying the given query.
+
+ Note that a ``False`` return value does not promise that there are more results, merely that the LMFDB is not guaranteeing that there are no more.
+
+ INPUT:
+
+ - ``table`` -- string, the name of the LMFDB table
+ - ``query`` -- dictionary, as parsed by psycodict's db._parse_dict
+ - ``db`` -- A db object for accessing the LMFDB (obtained either via lmfdb or lmfdb-lite)
+ - ``search_array`` -- a search array object for overriding null counts (optional)
+
+ OUTPUT:
+
+ - ``complete`` -- True if every object satisfying the constraints specified by the query
+ will be included in the search results
+
+ False if there may be objects missing (depending on the query, these objects may or may not exist)
+
+ None if the table has not implemented completeness guarantees
+
+ - ``reason`` -- A string, giving a reason why results are complete. Can be grammatically appended to "The LMFDB contains all". ``None`` if not ``complete``.
+
+ - ``caveat`` -- A string, giving any caveats (like dependence on GRH or unproven modularity theorems). May be ``None``.
+ """
+ if table in lookup:
+ return lookup[table].check(query, db, search_array)
+ return None, None, None
+
+
+#################################
+# Utility functions #
+#################################
+
+def nullcount_query(query, cols, recursing=False):
+ """
+ Returns a modified query with all reference to a given list of columns removed, and conditions added that the columns are null.
+ """
+ if isinstance(query, list): # can happen recursively in $or queries
+ return [nullcount_query(D, cols, recursing=True) for D in query]
+ query = dict(query)
+ for key, value in list(query.items()):
+ if key in ["$not", "$and", "$or"]:
+ L = nullcount_query(value, cols, recursing=True)
+ # Remove empty items
+ L = [D for D in L if D]
+ if L:
+ query[key] = L
+ else:
+ del query[key]
+ else:
+ if key.split(".")[0] in cols:
+ del query[key]
+ if not recursing:
+ for col in cols:
+ query[col] = None
+ return query
+
+
+def display_opts(L, conj="or"):
+ """
+ Display a list of options with commas and an or
+ """
+ L = [str(x) for x in L]
+ if len(L) > 1:
+ L[-1] = f" {conj} " + L[-1]
+ return ", ".join(L)
+
+
+def tup(a, b):
+ """
+ range(a, b) as a tuple
+ """
+ return tuple(range(a,b))
+
+
+def skip(a, b, L):
+ """
+ range(a, b) as a tuple, omitting elements of L
+ """
+ return tuple(x for x in range(a,b) if x not in L)
+
+
+#################################
+# NumberSets and IntegerSets #
+#################################
+
+# These objects add additional functionality on top of Sage's RealSet
+# can can be created from a query dictionary
+
+def to_rset(query):
+ """
+ Create a Sage RealSet from various inputs
+
+ Valid inputs:
+
+ * None (the whole real line)
+ * a RealSet or NumberSet
+ * a pair (list gives closed interval, tuple open interval)
+ * a set (the set of points)
+ * a single value (the corresponding point)
+ * a query dictionary (the RealSet described by the constraints)
+ """
+ if query is None:
+ return RealSet.interval(-infinity, infinity, lower_closed=False, upper_closed=False)
+ if isinstance(query, RealSet):
+ return query
+ if isinstance(query, NumberSet):
+ return query.rset
+ if isinstance(query, (list, tuple)) and len(query) == 2: # closed and open intervals
+ return RealSet(query)
+ if isinstance(query, set):
+ return RealSet(*[RealSet.point(x) for x in query])
+ if not isinstance(query, dict):
+ return RealSet.point(query)
+ ans = RealSet((-infinity, infinity))
+ for k, val in query.items():
+ if k == "$or":
+ ans = ans.intersection(RealSet(*[to_rset(D) for D in val]))
+ elif k == "$and":
+ ans = ans.intersection(to_rset(D) for D in val)
+ elif k in ["$not", "$ne"]:
+ ans = ans.intersection(to_rset(val).complement())
+ elif k == "$lte":
+ ans = ans.intersection(RealSet.unbounded_below_closed(val))
+ elif k == "$lt":
+ ans = ans.intersection(RealSet.unbounded_below_open(val))
+ elif k == "$gte":
+ ans = ans.intersection(RealSet.unbounded_above_closed(val))
+ elif k == "$gt":
+ ans = ans.intersection(RealSet.unbounded_above_open(val))
+ elif k == "$in":
+ ans = ans.intersection(RealSet(*[RealSet.point(x) for x in val]))
+ elif k == "$nin":
+ ans = ans.intersection(RealSet(*[RealSet.point(x) for x in val]).complement())
+ else:
+ raise ValueError(f"Unsupported key {k}")
+ return ans
+
+def interval_sum(I, J):
+ """
+ {i + j : i in I, j in J}
+ """
+ # Neither I nor J can be empty since they arise from a RealSet's normalized intervals
+ if not I or not J:
+ return (0, 0) # Empty interval
+ return RealSet.interval(I.lower() + J.lower(), I.upper() + J.upper(), lower_closed=(I.lower_closed() and J.lower_closed()), upper_closed=(I.upper_closed() and J.upper_closed()))
+
+def interval_neg(I):
+ """
+ {-i : i in I}
+ """
+ if not I:
+ return (0, 0)
+ return RealSet.interval(-I.upper(), -I.lower(), lower_closed=I.upper_closed(), upper_closed=I.lower_closed())
+
+Rneg = RealSet.unbounded_below_closed(0)[0]
+Rpos = RealSet.unbounded_above_closed(0)[0]
+inf_mone = RealSet.unbounded_below_closed(-1)[0]
+one_inf = RealSet.unbounded_above_closed(1)[0]
+
+def interval_mul(I, J):
+ """
+ {i * j : i in I, j in J}
+ """
+ if not I or not J or isinstance(I, tuple) and I == (0, 0) or isinstance(J, tuple) and J == (0, 0):
+ return (0, 0) # Empty interval
+
+ def _mul(A, B):
+ a0, a1, c0, c1 = A.lower(), A.upper(), A.lower_closed(), A.upper_closed()
+ b0, b1, d0, d1 = B.lower(), B.upper(), B.lower_closed(), B.upper_closed()
+ if a0 >= 0 and b0 >= 0: # both positive
+ a1b1 = 0 if a1 == 0 or b1 == 0 else a1 * b1 # one could be infinity
+ return RealSet.interval(a0 * b0, a1b1, lower_closed=c0 and d0, upper_closed=c1 and d1)
+ elif a1 <= 0 and b0 >= 0: # A negative
+ a0b1 = 0 if a0 == 0 or b1 == 0 else a0 * b1
+ a1b0 = 0 if a1 == 0 or b0 == 0 else a1 * b0
+ return RealSet.interval(a0b1, a1b0, lower_closed=c0 and d1, upper_closed=c1 and d0)
+ elif a0 >= 0 and b1 <= 0: # B negative
+ a1b0 = 0 if a1 == 0 or b0 == 0 else a1 * b0
+ a0b1 = 0 if a0 == 0 or b1 == 0 else a0 * b1
+ return RealSet.interval(a1b0, a0b1, lower_closed=c1 and d0, upper_closed=c0 and d1)
+ else: # both negative
+ a0b0 = 0 if a0 == 0 or b0 == 0 else a0 * b0
+ return RealSet.interval(a1 * b1, a0b0, lower_closed=c1 and d1, upper_closed=c0 and d0)
+
+ Ineg = I.intersection(Rneg)
+ Ipos = I.intersection(Rpos)
+ Jneg = J.intersection(Rneg)
+ Jpos = J.intersection(Rpos)
+ return RealSet(_mul(Ineg, Jneg), _mul(Ineg, Jpos), _mul(Ipos, Jneg), _mul(Ipos, Jpos))
+
+def interval_inv(I):
+ """
+ {1 / i : i in I, i != 0}
+ """
+ if not I or I.lower() == I.upper() == 0:
+ return (0, 0) # empty interval
+ Ineg = I.intersection(Rneg)
+ Ipos = I.intersection(Rpos)
+ ans = []
+ if Ineg.lower() != 0:
+ if Ineg.upper() == 0:
+ a, c = -infinity, False
+ else:
+ a, c = 1 / Ineg.upper(), Ineg.upper_closed()
+ if Ineg.lower() == -infinity:
+ b, d = 0, False
+ else:
+ b, d = 1 / Ineg.lower(), Ineg.lower_closed()
+ ans.append(RealSet.interval(a, b, lower_closed=c, upper_closed=d)[0])
+ if Ipos.upper() != 0:
+ if Ipos.lower() == 0:
+ b, d = infinity, False
+ else:
+ b, d = 1 / Ipos.lower(), Ipos.lower_closed()
+ if Ipos.upper() == infinity:
+ a, c = 0, False
+ else:
+ a, c = 1 / Ipos.upper(), Ipos.upper_closed()
+ ans.append(RealSet.interval(a, b, lower_closed=c, upper_closed=d)[0])
+ return RealSet(*ans)
+
+def interval_abs(I):
+ """
+ {|i| : i in I}
+ """
+ return RealSet(Rpos.intersection(I), interval_neg(Rneg.intersection(I)))
+
+class NumberSet:
+ """
+ A set of real numbers, as specified either as a number or a query dictionary.
+
+ Supports arithmetic operations, union, intersection and inequalities. The subclass IntegerSet supports iteration.
+ """
+ def __init__(self, x):
+ self.rset = to_rset(x)
+
+ def __repr__(self):
+ return repr(self.rset)
+
+ def __bool__(self):
+ return bool(self.rset)
+
+ def __add__(self, other):
+ return self.__class__(RealSet(*[interval_sum(I, J) for I in self.rset for J in other.rset]))
+
+ def __neg__(self):
+ return self.__class__(RealSet(*[interval_neg(I) for I in self.rset]))
+
+ def __sub__(self, other):
+ return self.__class__(RealSet(*[interval_sum(I, interval_neg(J)[0]) for I in self.rset for J in other.rset]))
+
+ def __mul__(self, other):
+ """
+ A set containing all products of elements in this set. The result will be sharp for real sets,
+ but may be proper for integer sets (for example, [2,4] * [2,4] = [4,16] and contains 5,7,10,11,13,14,15)
+ """
+ return self.__class__(RealSet(*[interval_mul(I, J) for I in self.rset for J in other.rset]))
+
+ def __invert__(self):
+ """
+ A set containing the inverse of elements in this set. For utility, this will always be a NumberSet,
+ even if the input is an IntegerSet.
+
+ We never raise a zero division error, instead implicitly intersecting with the complement of 0
+ """
+ return NumberSet(RealSet(*[interval_inv(I) for I in self.rset]))
+
+ def __truediv__(self, other):
+ """
+ A set containing the quotients of elements in this set.
+
+ We never raise a zero division error, instead implicitly intersecting other with the complement of 0
+ """
+ return NumberSet(
+ RealSet(*[interval_mul(I, interval_inv(J.intersection(Rneg))[0])
+ for I in self.rset for J in other.rset]).union(
+ RealSet(*[interval_mul(I, interval_inv(J.intersection(Rpos))[0])
+ for I in self.rset for J in other.rset])))
+
+ def __abs__(self):
+ return self.__class__(RealSet(*[interval_abs(I) for I in self.rset]))
+
+ def pow_cap(self, other, k):
+ """
+ Intersection of self with (-oo,max(other)^k]
+ """
+ if not other.rset:
+ return other
+ a = other.rset.sup()
+ if a is infinity:
+ return self
+ return self.intersection(top(a**k))
+
+ def union(self, *others):
+ return self.__class__(self.rset.union(*[other.rset for other in others]))
+
+ def intersection(self, *others):
+ return self.__class__(self.rset.intersection(*[other.rset for other in others]))
+
+ def difference(self, *others):
+ return self.__class__(self.rset.difference(*[other.rset for other in others]))
+
+ def is_subset(self, other):
+ return self.rset.is_subset(other.rset)
+
+ def __le__(self, other):
+ """
+ Every element of this set is less than or equal to every element of the other set
+ """
+ if not self.rset or not other.rset:
+ return True
+ a = self.rset[-1].upper()
+ b = other.rset[0].lower()
+ return a <= b
+
+ def __lt__(self, other):
+ """
+ Every element of this set is less than every element of the other set
+ """
+ if not self.rset or not other.rset:
+ return True
+ a = self.rset[-1].upper()
+ b = other.rset[0].lower()
+ return a < b or a == b and (not self.rset[-1].upper_closed() or not other.rset[0].lower_closed())
+
+ def __ge__(self, other):
+ """
+ Every element of this set is greater than or equal to every element of the other set
+ """
+ return other.__le__(self)
+
+ def __gt__(self, other):
+ """
+ Every element of this set is greater than every element of the other set
+ """
+ return other.__gt__(self)
+
+ def bounded(self, a, b=None):
+ """
+ If only a given, whether contained in (-oo, a].
+
+ If a and b given, whether contained in [a, b]
+ """
+ if b is None:
+ lower_bound, upper_bound = -infinity, a
+ else:
+ lower_bound, upper_bound = a, b
+ S = self.rset
+ return not S or lower_bound <= S[0].lower() and S[-1].upper() <= upper_bound
+
+ def restricted(self):
+ """
+ Not the whole real line
+ """
+ return (len(list(self.rset)), self.rset.inf(), self.rset.sup()) != (1, -infinity, infinity)
+
+def integer_normalize(S):
+ """
+ INPUT:
+
+ - ``S`` -- a RealSet (normalized in the sense of real sets)
+
+ Output:
+
+ A RealSet ``T`` so that the set of integer points of S and T are the same, and
+
+ - all intervals of ``T`` have endpoints that are either infinite or integral and closed
+ - successive intervals have an integer in between
+ """
+ T = []
+ for I in S:
+ if I.lower() is -infinity:
+ a, c = -infinity, False
+ else:
+ a, c = ceil(I.lower()), True
+ if a == I.lower() and not I.lower_closed():
+ a += 1
+ if I.upper() is infinity:
+ b, d = infinity, False
+ else:
+ b, d = floor(I.upper()), True
+ if b == I.upper() and not I.upper_closed():
+ b -= 1
+ if a <= b:
+ Iint = RealSet.interval(a, b, lower_closed=c, upper_closed=d)[0]
+ # InternalRealIntervals are UniqueRepresentation, and 11.0 == 11, so we get annoying floats when doing interval division
+ # We solve this by replacing _lower and _upper with integral versions
+ if a is not -infinity:
+ Iint._lower = RLF(a)
+ if b is not infinity:
+ Iint._upper = RLF(b)
+ T.append(Iint)
+ if not T:
+ return RealSet()
+ TT = []
+ cur = T[0]
+ for I in T[1:]:
+ if I.lower() - cur.upper() > 1:
+ TT.append(cur)
+ cur = I
+ else:
+ cur = RealSet.interval(cur.lower(), I.upper(), lower_closed=cur.lower_closed(), upper_closed=I.upper_closed())[0]
+ TT.append(cur)
+ return RealSet(*TT)
+
+inf_mone = RealSet.unbounded_below_closed(-1)[0]
+one_inf = RealSet.unbounded_above_closed(1)[0]
+
+class IntegerSet(NumberSet):
+ """
+ The set of integer points in within a real set
+ """
+ def __init__(self, x):
+ self.rset = integer_normalize(to_rset(x))
+
+ def __truediv__(self, other):
+ """
+ An interval containing all integer quotients.
+
+ EXAMPLES::
+
+ sage: from lmfdb.utils.completeness import IntegerSet
+ sage: A = IntegerSet([2, 4]); A / A
+ [1, 2]
+ sage: B = IntegerSet([6, 9]); B / A
+ [2, 4]
+ """
+ if isinstance(other, IntegerSet):
+ return self.__class__(
+ RealSet(*[interval_mul(I, interval_inv(J.intersection(inf_mone))[0])
+ for I in self.rset for J in other.rset]).union(
+ RealSet(*[interval_mul(I, interval_inv(J.intersection(one_inf))[0])
+ for I in self.rset for J in other.rset])))
+
+ return super().__div__(other)
+
+ def min(self):
+ return self.rset.inf()
+
+ def max(self):
+ return self.rset.sup()
+
+ def __iter__(self):
+ for I in self.rset:
+ if I.lower() is -infinity:
+ if I.upper() is infinity:
+ yield from ZZ
+ else:
+ b = I.upper()
+ for n in NN:
+ yield b - n
+ elif I.upper() is infinity:
+ a = I.lower()
+ for n in NN:
+ yield a + n
+ else:
+ yield from range(I.lower(), I.upper() + 1)
+
+ def stickelberger(self, n, r2opts):
+ """
+ INPUT:
+
+ - ``n`` -- the degree, an integer
+ - ``r2opts`` -- an iterable with r2 options
+
+ OUTPUT:
+
+ An iterator over the possible discriminants within this set
+ """
+ if all(r2 % 2 == 1 for r2 in r2opts):
+ mod4 = [0,3]
+ div4 = [1,2]
+ elif all(r2 % 2 == 0 for r2 in r2opts):
+ mod4 = [0,1]
+ div4 = [2,3]
+ else:
+ mod4 = [0,1,3]
+ div4 = [1,2,3]
+ for m in self:
+ if m % 4 in mod4:
+ if n == 2 and m % 4 == 0 and (m // 4) % 4 not in div4:
+ continue
+ F = factor(m)
+ if n != 2 or all(e == 1 or p == 2 and e <= 3 for (p,e) in F):
+ yield tuple(p for (p,e) in F)
+
+ def is_finite(self):
+ return self.rset.inf() is not -infinity and self.rset.sup() is not infinity
+
+ def bound_under(self, func):
+ """
+ INPUT:
+
+ - ``func`` -- an iterable of pairs (I, v) where I is valid input to IntegerSet and v is a real number
+
+ OUTPUT:
+
+ If this set is contained in the union of the I, the minimum of the values corresponding to the I used to greedily cover this set. If this set is not contained in the union of the I, returns None.
+ """
+ M = infinity
+ for I, v in func:
+ I = IntegerSet(I)
+ O = self.intersection(I)
+ if O:
+ M = min(M, v)
+ self = self.difference(O)
+ if not self.rset:
+ return M
+
+def top(x, cls=IntegerSet):
+ """
+ Utility function returnin (-oo, x]
+ """
+ return cls(RealSet.interval(-infinity, x, lower_closed=False, upper_closed=True))
+
+def bottom(x, cls=IntegerSet):
+ """
+ Utility function returning [x, oo)
+ """
+ return cls(RealSet.interval(x, infinity, lower_closed=True, upper_closed=False))
+
+#################################
+# Completeness checker #
+#################################
+
+class CompletenessChecker:
+ """
+ A class associated to an LMFDB designed to check whether search queries are complete.
+
+ The main entrypoint is the ``check`` function.
+
+ INPUT:
+
+ - ``table`` -- string, the name of the LMFDB table
+ - ``checkers`` -- a list of tuples, each of the form (cols, test), (cols, test, reason), (cols, test, reason, caveat) or (cols, test, reason, caveat, filter).
+ ``cols`` can be a single column name or a tuple of columns names.
+ ``reason`` is a string that describes the reason this query is complete
+ ``caveat`` is a string that describes any conjectures used, or None
+ ``filter`` is a function that takes the query as input and determines
+ whether this test should be run
+ ``test`` will be run if all columns are present in the query and the filter (if present) passes;
+ the input will be the correponding values in the query dictionary.
+ The check returns true when any test passes.
+
+ If all checkers have length 2 (just cols and test) will pass the full query dictionary to the __call__ method (after parsing through $or, $and, $not). Otherwise, will extract the values of the columns before passing into __call__
+ """
+ def __init__(self, table, checkers, fill=[], null_override=[]):
+ self.table = table
+ lookup[table] = self
+ self.extract = not all(len(check) == 2 for check in checkers)
+ for i, check in enumerate(checkers):
+ if len(check) == 2:
+ cols, test = check
+ reason = caveat = None
+ def filt(query): return True
+ elif len(check) == 3:
+ cols, test, reason = check
+ caveat = None
+ def filt(query): return True
+ elif len(check) == 4:
+ cols, test, reason, caveat = check
+ def filt(query): return True
+ else:
+ cols, test, reason, caveat, filt = check
+ if not isinstance(cols, tuple):
+ cols = (cols,)
+ checkers[i] = (cols, test, reason, caveat, filt)
+ self.checkers = checkers
+ self.fill = fill
+ self.null_override = null_override
+
+ def _standardize(self, query):
+ """
+ Different queries can yield equivalent logical expressions.
+ If ``$or`` is present, this function moves everything into the ``$or``
+ for simplified processing.
+ """
+ query = dict(query)
+
+ def merge(v1, v2):
+ if v1 is None:
+ return v2
+ # TODO: Make this more robust
+ if isinstance(v1, dict) and isinstance(v2, dict):
+ v2 = dict(v2)
+ v2.update(v1)
+ return v2
+ if isinstance(v1, dict):
+ # Actually need to check that v2 satisfies v1
+ return v2
+ if isinstance(v2, dict):
+ # Need to check that v1 satisfies v2
+ return v1
+ if v1 == v2:
+ return v2
+ # Need a way to show incompatibility
+ if "$or" in query:
+ # Should probably recurse here
+ opts = query["$or"]
+ for k, v in query.items():
+ if k != "$or":
+ for D in opts:
+ D[k] = merge(D.get(k), v)
+ query = {"$or": opts}
+ # Need to handle $and, $not as well
+ return query
+
+ def check(self, query, db, search_array=None):
+ if not query:
+ return False, None, None
+ query = self._standardize(query)
+ if "$or" in query:
+ reasons, caveats = set(), set()
+ for D in query["$or"]:
+ D = dict(D)
+ assert len(set(D).intersection(query)) == 0
+ D.update(query)
+ del D["$or"]
+ ok, reason, caveat = self.check(D, db, search_array)
+ if not ok:
+ return False, None, None
+ reasons.add(reason)
+ if caveat is not None:
+ caveats.add(caveat)
+ if caveats:
+ caveats = ", ".join(caveats)
+ else:
+ caveats = None
+ return True, "; ".join(reasons), caveats
+ if "$and" in query:
+ assert len(query) == 1
+ for D in query["$and"]:
+ ok, reason, caveat = self.check(D, db, search_array)
+ if ok:
+ return ok, reason, caveat
+ return False, None, None
+ # Ignore $not: it just imposes additional constraints, and if we're complete without it then we're complete. Note that it is accounted for in _columns_searched
+ table = db[self.table]
+ nulls = table.stats.null_counts()
+ if nulls:
+ search_columns = set(nulls).intersection(table._columns_searched(query)).difference(self.null_override)
+ # Ignore columns based on search_array
+ if search_array is not None:
+ search_columns = {col for col in search_columns if search_array.null_column_explanations.get(col) is not False}
+ if search_columns and table.exists(nullcount_query(query, search_columns)):
+ # Query referred to a column where not all data was computed, so we cannot guarantee completeness
+ return False, None, None
+ for fill in self.fill:
+ fill(query)
+ for cols, test, reason, caveat, filt in self.checkers:
+ if all(col in query for col in cols) and filt(query):
+ if self.extract:
+ # In this case, we use the reason specified in the list of checkers
+ if test(db, [query[col] for col in cols]):
+ return True, reason, caveat
+ else:
+ # Here we delegate the reason and caveat to the test function
+ return test(db, query)
+ return False, None, None
+
+#################################
+# Column tests #
+#################################
+
+# These objects are designed to be used as inputs for the ``test`` argument of the ``check`` method on a CompletnessChecker
+
+class ColTest:
+ pass
+
+class Bound(ColTest):
+ """
+ Check that the inputs lie in a box.
+ """
+ def __init__(self, *bounds, cls=IntegerSet):
+ self.cls = cls
+ self.bounds = [cls(b) if isinstance(b, (list, tuple, RealSet)) else cls(RealSet.unbounded_below_closed(b)) for b in bounds]
+
+ def __call__(self, db, Ds):
+ return all(self.cls(D).is_subset(B) for D, B in zip(Ds, self.bounds))
+
+class CBound(Bound):
+ """
+ Given constraints on a set of values, check that the last value lies in an interval
+
+ Note that overlapping Bound boxes is better when applicable,
+ since this test will only match queries where the constraints are specified exactly
+ """
+ def __init__(self, *constraints, cls=IntegerSet):
+ self.constraints = tuple(constraints[:-1])
+ super().__init__(constraints[-1], cls=cls)
+
+ def __call__(self, db, Ds):
+ return self.constraints == tuple(Ds[:-1]) and super().__call__(db, [Ds[-1]])
+
+class PrimeBound(Bound):
+ def __call__(self, db, Ds):
+ Ds = [self.cls(D) for D in Ds]
+ return all(D.is_finite() and all(is_prime(p) for p in D) for D in Ds)
+
+class Smooth(ColTest):
+ def __init__(self, M, cls=IntegerSet):
+ self.cls = cls
+ self.M = M
+
+ def __call__(self, db, ms):
+ M = self.M
+ P = prime_range(M)
+
+ def is_smooth(n):
+ return -M < n < M or n == prod(p**ZZ(n).valuation(p) for p in P)
+ return all(is_smooth(n) for n in self.cls(ms[0]))
+
+class Specific(ColTest):
+ def __init__(self, *constraints):
+ self.constraints = constraints
+
+ def __call__(self, db, Ds):
+ return all(D in constraint for (D, constraint) in zip(Ds, self.constraints))
+
+
+class CPrimeBound(CBound):
+ """
+ Similar to CBound, but requires Ds to all be prime
+ """
+ def __call__(self, db, Ds):
+ last = self.cls(Ds[-1])
+ return last.is_finite() and super().__call__(db, Ds) and all(is_prime(p) for p in last)
+
+#################################
+# Fillers #
+#################################
+
+# These classes are used to fill entries in for a query dictionary that can be derived from other entries
+
+class FieldLabelFiller:
+ def __init__(self, ec):
+ self.ec = ec
+
+ def __call__(self, query):
+ if "field_label" in query:
+ label = query["field_label"]
+ if isinstance(label, str) and label.count(".") == 3:
+ d, r, D, i = label.split(".")
+ if d.isdigit() and r.isdigit() and D.isdigit():
+ if "signature" not in query:
+ query["signature"] = [int(d), int(r)]
+ if self.ec and "abs_disc" not in query:
+ query["abs_disc"] = int(D)
+ elif not self.ec and "field_disc" not in query:
+ query["field_disc"] = -int(D)
+
+
+class MulFiller:
+ def __init__(self, n, e, f, backfill=False, cls=IntegerSet):
+ self.n, self.e, self.f = n, e, f
+ self.backfill, self.cls = backfill, cls
+
+ def __call__(self, query):
+ C = self.cls
+ n, e, f = self.n, self.e, self.f
+ if e in query and f in query:
+ query[n] = C(query.get(n)).intersection(C(query[e]) * C(query[f]))
+ if self.backfill:
+ if n in query and e in query:
+ query[f] = C(query.get(f)).intersection(C(query[n]) / C(query[e]))
+ if n in query and f in query:
+ query[e] = C(query.get(e)).intersection(C(query[n]) / C(query[f]))
+
+
+class SumFiller:
+ def __init__(self, a, b, c, backfill=False, cls=IntegerSet):
+ self.a, self.b, self.c = a, b, c
+ self.backfill, self.cls = backfill, cls
+
+ def __call__(self, query):
+ C = self.cls
+ a, b, c = self.a, self.b, self.c
+ if b in query and c in query:
+ query[a] = C(query.get(a)).intersection(C(query[b]) + C(query[c]))
+ if self.backfill:
+ if a in query and b in query:
+ query[c] = C(query.get(c)).intersection(C(query[a]) - C(query[b]))
+ if a in query and c in query:
+ query[b] = C(query.get(b)).intersection(C(query[a]) - C(query[c]))
+
+
+class CMFFiller:
+ def __call__(self, query):
+ C = IntegerSet
+ if query.get("projective_image"):
+ query["weight"] = 1
+ # TODO: set weigt/level from analytic conductor
+ N, k = query.get("level"), query.get("weight")
+ if N is not None and k is not None:
+ query["Nk2"] = C(N) * C(k) * C(k)
+ if query.get("char_orbit_index") == 1 or query.get("prim_orbit_index") == 1:
+ query["char_order"] = 1
+
+
+#################################
+# Specific CompletenessCheckers #
+#################################
+
+# This section contains completeness checkers tailored for specific LMFDB tables,
+# for cases when the previous generic options are not sufficient.
+
+#### Maass forms ####
+
+# We cache the intervals for Maass forms, since we'll need to reimplement this function anyway if the db gets expanded (any will likely include other weights/characters)
+maxR = {
+ 1: 184.9239,
+ 2: 25.1193,
+ 3: 24.9526,
+ 5: 24.3767,
+ 6: 25.8128,
+ 7: 24.8119,
+ 10: 26.2206,
+ 11: 25.1115,
+ 13: 24.1069,
+ 14: 22.4246,
+ 15: 21.8123,
+ 17: 21.6894,
+ 19: 20.5855,
+ 21: 19.9897,
+ 22: 19.4628,
+ 23: 19.5899,
+ 26: 19.0296,
+ 29: 17.8751,
+ 30: 18.4508,
+ 31: 18.0663,
+ 33: 18.3093,
+ 34: 18.1504,
+ 35: 18.2470,
+ 37: 15.9750,
+ 38: 17.8932,
+ 39: 17.6166,
+ 41: 15.9529,
+ 42: 17.2648,
+ 43: 14.7934,
+ 46: 16.9195,
+ 47: 13.9142,
+ 51: 16.5449,
+ 53: 14.1707,
+ 55: 16.1894,
+ 57: 16.2665,
+ 58: 16.1047,
+ 59: 12.8281,
+ 61: 11.3236,
+ 62: 15.9467,
+ 65: 16.0760,
+ 66: 15.7452,
+ 67: 10.2802,
+ 69: 15.8350,
+ 70: 15.5741,
+ 71: 9.6346,
+ 73: 8.7967,
+ 74: 15.5368,
+ 77: 15.7849,
+ 78: 15.3004,
+ 79: 6.9669,
+ 82: 15.3011,
+ 83: 7.3430,
+ 85: 15.5549,
+ 86: 15.1230,
+ 87: 15.2365,
+ 89: 6.1712,
+ 91: 15.1940,
+ 93: 15.0326,
+ 94: 15.2433,
+ 95: 15.1521,
+ 97: 5.8923,
+ 101: 7.2205,
+ 102: 14.6673,
+ 103: 5.6533,
+ 105: 14.7330
+}
+class MaassBound(ColTest):
+ def __call__(self, db, query):
+ level = IntegerSet(query["level"])
+ spectral_parameter = NumberSet(query["spectral_parameter"])
+ if level.is_finite() and level.max() <= 105:
+ levels = list(level)
+ if levels:
+ if all(N in maxR for N in levels):
+ Rbound = min(maxR[N] for N in levels)
+ if spectral_parameter.bounded(Rbound):
+ levels = ", ".join(str(N) for N in levels)
+ return True, f"Maass forms with level {levels} and spectral parameter at most {Rbound:.4f}", None
+ else:
+ return True, "Maass forms with specified level (empty)", None
+ return False, None, None
+
+
+#### Hilbert modular forms ####
+
+@cached_function
+def hmf_bounds(db):
+ return {label: D["max"] for (label, D) in db.hmf_forms.stats.numstats("level_norm", "field_label").items()}
+
+
+class HMFBound(ColTest):
+ def __call__(self, db, query):
+ bounds = hmf_bounds(db)
+ level_norm = IntegerSet(query["level_norm"])
+ cols = db.hmf_forms._columns_searched(query)
+ caveat = []
+ if "is_base_change" in cols:
+ caveat.append("positive base change information computed heuristically")
+ if "is_CM" in cols:
+ caveat.append("positive CM information computed heuristically")
+ caveat = ", ".join(caveat)
+ if not caveat:
+ caveat = None
+ if "field_label" in query:
+ label = query["field_label"]
+ M = None
+ if isinstance(label, str):
+ M = bounds[label]
+ labels = label
+ elif isinstance(label, dict) and "$in" in label and label["$in"] and all(lab in bounds for lab in label["$in"]):
+ M = min(bounds[lab] for lab in label["$in"])
+ labels = ", ".join(label["$in"])
+ if M is not None and level_norm.bounded(M):
+ return True, f"Hilbert modular forms over {labels} of level norm at most {M}", caveat
+ # Missing discriminants in degree
+ # 2: up to 497 except 253, 257, 264, 309, 312
+ # 3: everything up to disc 1957 (next 2021)
+ # 4: everything up to disc 19821 (next 20032)
+ # 5: everything up to disc 195829 (next 202817)
+ # 6: everything up to disc 1997632 (next 2115281)
+ by_deg = {2: 252, 3: 2020, 4: 20031, 5: 202816, 6: 2115280}
+ if query.get("disc") is not None and query.get("deg") is not None:
+ disc = IntegerSet(query["disc"])
+ deg = IntegerSet(query["deg"])
+ if deg.is_subset(IntegerSet([2,6])): #################
+ deg = list(deg)
+ if not deg:
+ return True, "Hilbert modular forms satisfying query (no matching degrees)", None
+ M = min(by_deg[n] for n in deg)
+ if disc.bounded(M):
+ # query specifies only fields where we have HMFs,
+ # now we need to find the list of discriminants satisfying the query
+ allowed = IntegerSet({"$in":set(int(label.split(".")[2]) for label in bounds)})
+ discs = list(disc.intersection(allowed))
+ fields = [label for label in bounds if int(label.split(".")[0]) in deg and int(label.split(".")[2]) in discs]
+ if fields:
+ M = min(bounds[label] for label in fields)
+ if level_norm.bounded(M):
+ fields.sort(key=lambda label: [int(x) for x in label.split(".")])
+ labels = ", ".join(fields)
+ return True, f"Hilbert modular forms over {labels} of level norm at most {M}", caveat
+ else:
+ return True, "Hilbert modular forms satisfying the query (no matching fields)", caveat
+ return False, None, None
+
+
+#### Bianchi modular forms and elliptic curves over number fields ####
+
+class BianchiBound(ColTest):
+ def __init__(self, ec):
+ self.ec = ec
+
+ def __call__(self, db, query):
+ if self.ec:
+ sig = query.get("signature")
+ if not (isinstance(sig, list) and len(sig) == 2):
+ return False, None, None
+ n, r = sig
+ D = IntegerSet(query.get("abs_disc")).intersection(bottom(3))
+ N = IntegerSet(query["conductor_norm"])
+ caveat = "Only modular elliptic curves are included"
+
+ def reason(n, r, D, M):
+ if isinstance(D, int):
+ Ds = str(D)
+ elif D.max() == D.min():
+ Ds = str(D.max())
+ else:
+ Ds = f" in {D}"
+ if n == 2 and r == 0:
+ return f"elliptic curves with conductor norm at most {M} over imaginary quadratic fields with absolute discriminant {Ds}"
+ if r == n:
+ if n == 2:
+ return f"elliptic curves with conductor norm at most {M} over real quadratic fields with discriminant {Ds}"
+ if n == 3:
+ adj = "cubic"
+ elif n == 4:
+ adj = "quartic"
+ elif n == 5:
+ adj = "quintic"
+ elif n == 6:
+ adj = "sextic"
+ return f"elliptic curves with conductor norm at most {M} over totally real {adj} fields with discriminant {Ds}"
+ if n == 3 and r == 1:
+ return f"elliptic curves over 3.1.23.1 with conductor norm at most {M}"
+ else:
+ n, r = 2, 0
+ D = (-IntegerSet(query.get("field_disc"))).intersection(bottom(3))
+ N = IntegerSet(query["level_norm"])
+ caveat = None
+
+ def reason(n, r, D, M):
+ if not D:
+ return "Bianchi modular forms with specified discriminant (no fields with given discriminants)"
+ if D.max() == D.min():
+ Ds = str(D.max())
+ else:
+ Ds = f"in {D}"
+ return f"Bianchi modular forms with level norm at most {M} over imaginary quadratic fields with absolute discriminant {Ds}"
+ if n == 2 and r == 0: # imaginary quadratic, either EC or BMF
+ M = D.bound_under([
+ (top(3), 150000), # 3
+ (4, 100000), # 4
+ ([5,14], 50000), # 7,8,11
+ (15, 1000), # 15
+ ([16,19], 15000), # 19
+ (20, 1000), # 20
+ ([21,23], 3000), # 23
+ (24, 1000), # 24
+ ([25,34], 5000), # 31
+ ([35,40], 1000), # 35,39,40
+ ([41,46], 15000), # 43
+ ([47, 59], 1000), # 47,51,52,55,56,59
+ ([60, 67], 10000), # 67
+ ([68, 120], 1000),
+ ([121, 159], 100),
+ ([160, 163], 5000), # 163
+ ([164, 702], 100), # 703 is the first missing
+ ])
+ if M is None or not N.bounded(M):
+ return False, None, None
+ return True, reason(n, r, D, M), caveat
+ if r == n: # totally real, EC
+ if n == 2:
+ return D.bounded(497) and N.bounded(5000), reason(2, 2, 497, 5000), None
+ if n == 3:
+ return D.bounded(1957) and N.bounded(2059), reason(3, 3, 1957, 2059), None
+ if n == 4:
+ return D.bounded(19821) and N.bounded(4091), reason(4, 4, 19821, 4091), None
+ if n == 5:
+ return D.bounded(195829) and N.bounded(1013), reason(5, 5, 195829, 1013), None
+ if n == 6:
+ return D.bounded(1997632) and N.bounded(961), reason(6, 6, 1997632, 961), None
+ if n == 3 and r == 1: # mixed case, EC
+ return D.bounded(30) and N.bounded(20000), reason(3, 1, 23, 20000), None
+ return False, None, None
+
+
+#### Number fields ####
+
+class NFBound(ColTest):
+ def __init__(self):
+ # maxD[n][r2] is an integer M so that we have completeness in signature [n-2*r2, r2] as long as the absolute discriminant is at most M.
+ self._maxD = [
+ None, # n=0
+ None, # n=1
+ [2*10**6, 2*10**6], # n=2
+ [150**3, 150**3], # n=3
+ [10**7, 4*10**6, 4*10**6], # n=4
+ [10**8, 12*10**6, 12*10**6], # n=5
+ [28**6, 10**7, 10**7, 10**7], # n=6
+ [214942297, 2*10**8, 2*10**8, 2*10**8], # n=7
+ [17**8, 79259702, 20829049, 5726300, 1656109], # n=8
+ [15**9, 27316369, 27316369, 146723910, 39657561], # n=9
+ [190612177]*6, # n=10
+ [5154074557]*6, # n=11
+ [37250695278]*7, # n=12
+ ]
+
+ # num_trans[n] is the number of transitive permutation groups in degree n
+ self._num_trans = [0, 1, 1, 2, 5, 5, 16, 7, 50, 34, 45, 8, 301, 9, 63, 104, 1954, 10, 983, 8, 1117, 164, 59, 7]
+
+ # ab.get(n, {1}) gives the values of t so that nTt is abelian (for n<48)
+ self._ab = {
+ 4: {1,2},
+ 8: {1,2,3},
+ 9: {1,2},
+ 12: {1,2},
+ 16: {1,2,3,4,5},
+ 18: {1,2},
+ 20: {1,3},
+ 24: {1,2,3},
+ 25: {1,2},
+ 27: {1,2,4},
+ 28: {1,2},
+ 32: {32,33,34,36,37,39,43},
+ 36: {1,2,3,4},
+ 40: {1,2,7},
+ 44: {1,2},
+ 45: {1,2}}
+
+ # nab_gal[n] gives the values of t so that nTt is nonabelian of order n (for n<24)
+ self._nab_gal = {
+ 6: {2},
+ 8: {4,5},
+ 10: {2},
+ 12: {3,4,5},
+ 14: {2},
+ 16: {6,7,8,9,10,11,12,13,14},
+ 18: {3,4,5},
+ 20: {2,4,5},
+ 21: {2},
+ 22: {2}}
+
+ # nsolv.get(n, set()) gives the values of t so that nTt is nonsolvable (for n<24)
+ self._nsolv = {
+ 5: {4,5},
+ 6: {12,14,15,16},
+ 7: {5,6,7},
+ 8: {37,43,48,49,50},
+ 9: {27,32,33,34},
+ 10: {7,11,12,13,22,26,30,31,32,34,35,36,37,38,39,40,41,42,43,44,45},
+ 11: {5,6,7,8},
+ 12: {33,74,75,76,123,124,179,180,181,182,183,218,219,220,230,255,256,257,269,270,272,277,278,279,285,286,287,288,293,295,296,297,298,299,300,301},
+ 13: {7,8,9},
+ 14: {10,16,17,19,30,33,34,39,42,43,46,47,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63},
+ 15: {5,10,15,16,20,21,22,23,24,28,29,47,53,61,62,63,69,70,72,76,77,78,83,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104},
+ 16: {713,714,715,1035,1036,1080,1081,1328,1329,1504,1505,1506,1507,1508,1653,1654,1753,1801,1802,1803,1804,1805,1838,1839,1840,1842,1843,1844,1861,1873,1878,1882,1883,1902,1903,1906,1916,1938,1940,1944,1945,1946,1948,1949,1950,1951,1952,1953,1954},
+ 17: {6,7,8,9,10},
+ 18: {90,144,145,146,227,260,261,262,362,363,364,365,377,427,452,468,596,664,665,666,722,723,736,787,788,789,790,791,802,845,846,847,848,849,855,856,886,887,888,890,897,898,899,900,911,913,914,925,933,934,935,936,937,938,946,947,948,949,950,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983},
+ 19: {7,8},
+ 20: {15,30,31,32,35,36,62,63,64,65,66,70,89,116,117,118,119,120,123,145,146,147,148,149,150,151,152,172,174,175,176,177,197,198,199,200,201,202,203,204,205,206,207,208,217,218,219,220,221,222,223,224,225,226,227,228,229,230,264,265,266,267,272,273,274,275,276,277,278,279,280,281,283,284,285,287,288,289,290,291,358,362,363,365,366,367,368,369,370,373,375,376,452,453,456,457,458,459,460,461,466,467,468,531,532,539,540,541,542,543,544,545,546,547,548,555,556,558,560,561,562,564,565,566,567,568,569,570,571,573,635,654,655,656,657,658,659,663,664,665,666,667,668,669,671,672,673,674,675,676,677,679,680,681,682,684,685,686,687,688,689,690,691,692,693,694,695,752,753,754,781,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,812,855,856,857,858,885,886,887,888,912,913,914,915,916,917,918,919,920,921,922,933,934,935,936,937,938,939,947,948,949,950,951,952,953,954,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,981,985,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1006,1007,1008,1009,1010,1011,1012,1013,1015,1016,1019,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1044,1045,1046,1047,1048,1052,1053,1054,1058,1059,1060,1061,1062,1063,1064,1065,1066,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117},
+ 21: {14,20,22,27,33,38,44,56,57,58,67,74,85,91,103,104,111,113,115,118,119,121,125,126,128,129,130,132,135,136,138,139,140,141,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164},
+ 22: {13,14,22,26,27,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59},
+ 23: {5,6,7}
+ }
+
+ # rdgrd[n][t] gives a ratio r so that grd <= rd^(1/r) for fields with Galois group nTt
+ # See https://arxiv.org/abs/1208.5806
+ self._rdgrd = [
+ [],
+ [1], # 1
+ [1], # 2
+ [1, 2/3], # 3
+ [1, 1, 1/2, 3/4, 1/2], # 4
+ [1, 4/5, 4/5, 3/5, 2/5], # 5
+ [1, 1, 2/3, 2/3, 1/2, 1/3, 2/3, 2/3, 1/2, 1/2, 1/3, 2/3, 1/3, 2/3, 1/2, 1/3], #6
+ [1, 6/7, 6/7, 6/7, 4/7, 3/7, 2/7], # 7
+ [1, 1, 1, 1, 1, 3/4, 1/2, 3/4, 1/2, 1/2, 1/2, 3/4, 3/4, 3/4, 1/2, 1/2,
+ 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 3/4, 1/2, 7/8, 1/2, 1/4, 1/2, 1/2, 1/2,
+ 1/4, 1/2, 1/2, 1/2, 1/4, 3/4, 3/4, 1/4, 1/2, 1/2, 1/2, 3/8, 3/4, 1/4,
+ 3/8, 3/8, 1/4, 1/2, 3/8, 1/4], #8
+ [1, 1, 8/9, 2/3, 8/9, 2/3, 2/3, 2/3, 8/9, 2/3, 2/3, 2/3, 2/3, 8/9, 8/9,
+ 2/3, 1/3, 2/3, 2/3, 1/3, 1/3, 1/3, 2/3, 1/3, 1/3, 2/3, 7/9, 2/9, 1/3,
+ 1/3, 2/9, 2/3, 1/3, 2/9], #9
+ [1, 1, 4/5, 4/5, 4/5, 1/2, 4/5, 2/5, 1/2, 1/2, 3/5, 3/5, 3/5, 1/5, 2/5,
+ 2/5, 1/2, 1/2, 1/2, 1/2, 2/5, 2/5, 1/5, 2/5, 2/5, 4/5, 2/5, 2/5, 1/5,
+ 4/5, 4/5, 3/5, 2/5, 2/5, 3/5, 1/5, 2/5, 2/5, 1/5, 3/10, 3/10, 3/10,
+ 1/5, 3/10, 1/5], # 10
+ [1, 10/11, 10/11, 10/11, 8/11, 8/11, 3/11, 2/11], # 11
+ [1, 1, 1, 1, 1, 2/3, 2/3, 5/6, 2/3, 2/3, 2/3, 5/6, 5/6, 1/2, 1/2, 1/2,
+ 1/2, 1/2, 1/2, 3/4, 1/3, 2/3, 2/3, 2/3, 1/3, 2/3, 2/3, 1/2, 1/3, 1/3,
+ 2/3, 2/3, 5/6, 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 2/3, 3/4,
+ 1/2, 2/3, 2/3, 1/3, 2/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3,
+ 1/2, 2/3, 1/2, 2/3, 2/3, 2/3, 2/3, 2/3, 2/3, 2/3, 1/3, 1/2, 1/2, 1/2,
+ 1/2, 2/3, 2/3, 2/3, 1/3, 1/2, 1/3, 1/2, 1/2, 1/2, 1/2, 2/3, 2/3, 1/3,
+ 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3,
+ 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/2, 1/2, 2/3, 2/3, 1/2,
+ 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 1/2, 2/3, 2/3, 2/3, 1/3, 1/3, 2/3, 2/3,
+ 1/2, 1/4, 1/4, 1/2, 1/2, 1/6, 1/6, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3,
+ 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/2,
+ 2/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/2, 1/3, 1/4, 1/4, 1/4, 1/4,
+ 1/4, 1/4, 1/4, 1/4, 1/2, 1/2, 1/2, 1/2, 5/6, 1/2, 2/3, 2/3, 1/2, 1/3,
+ 1/3, 1/3, 1/3, 1/6, 1/3, 1/3, 1/3, 1/3, 1/6, 1/4, 1/3, 1/3, 1/3, 1/3,
+ 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/6, 1/4, 1/4, 1/4, 1/4,
+ 1/2, 1/4, 1/4, 1/4, 1/4, 5/6, 1/3, 2/3, 1/3, 1/6, 1/3, 1/6, 1/3, 1/3,
+ 1/6, 1/3, 1/3, 1/3, 1/4, 1/4, 1/4, 1/4, 1/3, 1/3, 1/3, 1/3, 1/3, 1/6,
+ 1/6, 1/4, 1/4, 1/4, 1/4, 1/4, 1/4, 1/4, 1/4, 1/6, 1/3, 1/3, 1/3, 1/3,
+ 1/6, 1/3, 1/3, 1/4, 1/4, 1/6, 1/6, 1/4, 1/4, 1/6, 1/4, 1/4, 1/4, 1/3,
+ 1/3, 1/6, 1/4, 2/3, 1/4, 1/6, 1/4, 1/4, 1/3, 1/3, 1/3, 1/6, 1/4, 1/4,
+ 1/4, 1/4, 1/3, 1/6, 1/3, 1/3, 1/6, 1/4, 1/4, 1/6, 1/6, 1/6, 2/3, 1/4,
+ 1/4, 1/4, 1/6, 1/4, 1/6], # 12
+ [1, 12/13, 12/13, 12/13, 12/13, 12/13, 8/13, 3/13, 2/13], # 13
+ [1, 1, 6/7, 6/7, 6/7, 4/7, 6/7, 1/2, 3/7, 6/7, 4/7, 1/2, 1/2, 1/2, 1/2,
+ 4/7, 5/7, 3/7, 4/7, 3/7, 2/7, 1/2, 1/2, 1/2, 1/2, 3/7, 2/7, 2/7, 1/7,
+ 6/7, 3/7, 3/7, 4/7, 4/7, 2/7, 3/7, 3/7, 1/7, 6/7, 2/7, 2/7, 3/7, 3/7,
+ 1/7, 3/7, 3/7, 3/7, 1/7, 2/7, 2/7, 1/7, 2/7, 2/7, 2/7, 2/7, 1/7, 1/7,
+ 3/14, 3/14, 3/14, 1/7, 3/14, 1/7], # 14
+ [1, 14/15, 4/5, 2/3, 4/5, 4/5, 2/3, 4/5, 2/3, 4/5, 2/3, 2/3, 2/3, 2/3,
+ 4/5, 3/5, 2/3, 2/3, 2/3, 4/5, 4/5, 3/5, 3/5, 2/5, 1/3, 2/5, 2/3, 8/15,
+ 2/5, 1/3, 1/3, 1/3, 2/5, 2/5, 2/5, 1/5, 1/3, 1/3, 1/3, 1/3, 2/5, 2/5,
+ 2/5, 1/5, 1/5, 1/5, 4/5, 1/3, 1/3, 4/15, 1/3, 2/5, 2/5, 1/5, 1/5, 1/5,
+ 1/3, 4/15, 4/15, 4/15, 2/5, 2/5, 2/5, 1/5, 1/3, 1/3, 4/15, 4/15, 1/5,
+ 2/5, 1/5, 8/15, 4/15, 4/15, 4/15, 1/5, 1/5, 1/5, 1/5, 1/5, 2/15, 4/15,
+ 1/5, 1/5, 1/5, 2/15, 2/15, 1/5, 1/5, 2/15, 1/5, 1/5, 2/15, 1/5, 1/5,
+ 1/5, 1/5, 1/5, 1/5, 1/5, 2/15, 2/15, 1/5, 2/15], # 15
+ [1,1,1,1,1,1,1,1,1,1,1,1,1,1], #16
+ [1], #17
+ [1,1,1,1,1], #18
+ [1], #19
+ [1,1,1,1,1], #20
+ [1,1], #21
+ [1,1], #22
+ [1], #23
+ ]
+
+ od = 44.76323219095532388621866759
+ # grd[n] is a list of pairs(ts, M) so that we have completeness for fields with Galois group nTt (t in ts) as long as grd <= M
+ self._grd = {
+ 2: [((1,), 1500)],
+ 3: [((1,), 500),
+ ((2,), 250)],
+ 4: [((1,2,3), 200), # quad over quad
+ ((4,5), 150)],
+ 5: [((1,2), 200),
+ ((3,), 200),
+ ((4,5), 85)],
+ 6: [((12,14), 85), # pumped up
+ ((15,16), 60),
+ ((1,2,3,5,9), 200), # just did 6t3 as composita
+ ((10,), 150),
+ ((4,5,6,7,8,11,13), 150)],
+ 7: [((4,), 75),
+ ((1,3), 200),
+ ((2,), 200),
+ ((5,6), 45),
+ ((7,), 35)],
+ 8: [((3,5,23,24,39,40,41,44,45,46), 100),
+ ((4,6,8,9,10,15,17,18,19,26,28,29,30,35), 125), # over 4T3
+ ((1,2,7,16,20,27), 125), # over 4T1
+ ((12,13,32,38), 250), # over 4T4
+ ((11,21,22,31), 100), # over 4T2
+ ((14,33), 150),
+ ((25,36), 200), #prim
+ ((34,), 110),
+ ((37,), 45),
+ ((42,), 135),
+ ((47,), 150),
+ ((48,), 45)],
+ 9: [((16,), od),
+ ((26,), 75),
+ ((9,13,14,15,19,22,23,24,25,29), 100),
+ ((31,), 100),
+ ((18,), 150), # using 6T9
+ ((30,), 115),
+ ((1,2,4,6,7,17), 500), #C3 over C3
+ ((1,2,3,4,5,6,7,8,10,11,12,20,21,24,29,30,31), 200),
+ ((20,28), 150)],
+ 10: [(tup(1,28)+(29,32,34,36,37,38,39), od),
+ ((4,), 100), # F_5
+ ((20,), 50),
+ ((1,2,6), 200)], # C_5 over C_2
+ 11: [((1,), 200),
+ ((2,3), od),
+ ((4,), 22.5)],
+ 12: [((1,2,5), od),
+ (tup(1,20)+tup(21,43)+tup(48,70)+tup(74,83), od/2),
+ ((1,5), 150)],
+ 13: [((1,), 200),
+ ((2,), od),
+ ((3,4,5,6), od/2)],
+ 14: [((1,), od),
+ ((2,), 50),
+ ((3,4,5,6,7,8,9,10,11,17,18,19,21,27,28,29,33,34,35,38,40,41,42,43,44,47,48,50,51,53,56), 23)],
+ 15: [((1,2), od)],
+ 16: [((1,2,3,4,5), od),
+ (tup(6,57)+tup(67,178)+tup(197,414), od/2)],
+ 17: [((1,), 200),
+ ((2,), od)],
+ 18: [((1,2,3,4,5), 100)],
+ 19: [((1,), 200),
+ ((2,), od)],
+ 20: [((1,2,3,4,5), 100)],
+ 21: [((1,2), 100)],
+ 22: [((1,), 100)],
+ 23: [((1,2), od)],
+ }
+
+ quartic_2_group = (1,2,3)
+ octic_2_group = (1,2,3,4,5,6,7,8,9,10,11,15,16,17,18,19,20,21,22,26,27,28,29,30,31,35)
+ octwith4 = (1,2,4,6,7,8,10,12,13,14,16,17,19,20,21,23,27,28,30,38,40)
+ octic_with_quartic = tup(1,25)+tup(26,33)+(35,38,39,40,44)
+ octic_type_2 = (33,34,41,42,45,46,47)
+ decic_with_quint = (1,2,3,4,5,8,11,12,14,15,16,22,23,24,25,29,34,36,37,38,39)
+ decic_with_quad = (1,2,3,4,5,6,9,10,11,12,17,18,19,20,21,22,27,28,33,40,41,42,43)
+
+ # r2G[n] is a list of triples (r2, ts, M) so that we have all number fields with signature [n-2*r2, r2] and Galois group nTt (for t in ts) as long as the absolute discriminant is at most M (if M is None, there is no discriminant restriction since that signature/Galois group combination is impossible)
+ self._r2G = {
+ 3: [(1, (1,), None)],
+ 4: [(0, (1,2,3), 150**4),
+ (0, (4,), 10**10), # from megrez
+ (1, (1,2,4), None),
+ (1, (3,), 15**6),
+ (2, (1,2,3), 15**6),
+ (2, (4,), 10**10)], # from megrez
+ 5: [(0, (1,2), 10**10),
+ (0, (4,), 2**38),
+ (1, (1,2,3,4), None),
+ (2, (1,), None),
+ (2, (2,4), 10**8)],
+ 6: [# r2=0 bound for arbitrary t is 28^6
+ (0, (1,2), 250**6), # Base changing S_3 grd up, using Belabash, disc filter, moving them up; redid 6T2 by Rachel/grd, which picks up 6T1
+ (0, (3,), 10**11), # from Kluners
+ (0, (4,), 100**6), # from Kluners
+ (0, (5,), 10**10), # matches Kluners
+ (0, (6,), 50**6), # higher than Kluners
+ (0, (7,), 18**9), # Computed with a special version for even groups
+ (0, (8,), 10**12), # from Kluners
+ (0, (9,), 2*10**10), # from Kluners
+ (0, (10,), 10**11), # from Kluners
+ (0, (11,), 35**6),
+ (0, (12,), 2**38), # from A5 quintics pushed up
+ (0, (14,), 35**6), # from S5 quintics pushed up
+ # r2=1 bound for arbitrary t is 10^7
+ (1, (1,2,3,4,5,7,8,9,10,12,14,15), None),
+ (1, (6,), 10**9), # from Kluners
+ (1, (11,), 10**9), # from Kluners
+ (1, (13,), 10**8-1), # from Eric Driver
+ # r2=2 bound for arbitrary t is 10^7
+ (2, (1,2,5), None),
+ (2, (3,), 64**6), # Done by composita
+ (2, (4,), 100**6), # from Kluners
+ (2, (6,), 10**9), # from Kluners
+ (2, (7,), 18**9), # Computed with a special version for even groups
+ (2, (8,), 15**9), # JJ has double checked
+ (2, (9,10), 5*10**9), # from Eric Driver
+ (2, (11,), 10**8), # from Kluners
+ (2, (12,14), 35**6),
+ (2, (13,), 10**8-1), # from Eric Driver
+ # r2=3 bound for arbitrary t is 10^7
+ (3, (4,7,10,12,15), None),
+ (3, (1,2), 250**6), # Base changing S_3 grd up, using Belabash, disc filter, moving them up; redid 6T2 by Rachel/grd, which picks up 6T1
+ (3, (3,), 64**6), # Done by composita
+ (3, (5,), 10**10), # matches Kluners
+ (3, (6,), 10**9), # from Kluners
+ (3, (8,), 15**9), # JJ has double checked
+ (3, (9,), 5*10**9), # from Eric Driver
+ (3, (11,), 10**8), # from Kluners
+ (3, (13,), 10**8-1), # from Eric Driver
+ (3, (14,), 35**6)],
+ 7: [# r2=0 bound for arbitrary t is 214942297
+ (0, (3,), 26**7), # LMFDB
+ (0, (5,), 38**7), # LMFDB
+ (0, (6,), 988410720),
+ # r2=1,2,3 bound for arbitrary t is 2*10^8
+ (1, tup(1,7), None),
+ (2, tup(1,5), None),
+ (3, (1,3,5,6), None)],
+ 8: [# r2=0 bound for arbitrary t is 17^8
+ (0, (1,2), 150**8), # also 5
+ (0, (5,), 512**8), # LMFDB
+ (0, octic_with_quartic, 10**12),
+ (0, (4,), 55**8),
+ (0, (12,), 56**8),
+ (0, (37,), 30**8),
+ (0, (45,), 3*15**8),
+ # r2=1 bound for arbitrary t is 79259702
+ (1, skip(1,50,{27,32,35,38,44,47}), None),
+ (1, (27,32,35,38,44), 10**12),
+ (1, (47,), 3*10**9),
+ # r2=2 bound for arbitrary t is 20829049
+ (2, tup(1,7)+(8,12,13,14,23,25,36,37,43), None),
+ (2, octic_with_quartic, 4*10**10),
+ (2, octic_type_2, 3*10**9), # from Eric Driver, quartic over quadratic
+ (2, (45,), 3*15**8),
+ (2, (48,), 35831808), # special computation using 7t5 results
+ # r2=3 bound for arbitrary t is 5726300
+ (3, skip(1,50,{6,8,15,23,26,27,30,31,35,38,40,43,44,47}), None),
+ (3, octic_with_quartic, 144*10**8),
+ (3, (8,), 20**9),
+ (3, octic_type_2, 3*10**9), # from Eric Driver, quartic over quadratic
+ # r2=4 bound for arbitrary t is 1656109
+ (4, octic_with_quartic, 49*10**8),
+ (4, (4,), 55**8),
+ (4, (5,), 512**8), # LMFDB
+ (4, (8,), 20**9),
+ (4, octic_type_2, 3*10**9), # from Eric Driver, quartic over quadratic
+ (4, (36,), 15**8), # LMFDB
+ (4, (37,), 30**8),
+ (4, (45,), 3*15**8),
+ (4, (48,), 35831808)], # special computation using 7t5 results
+ 9: [# r2=0 bound for arbitrary t is 15^9
+ (0, (1,2,6,7,17), 50**9), # C3 over C3
+ (0, (3,10,11,21), 56**9),
+ (0, (4,8,12,20,29,31), 25**9),
+ (0, (5,), 20**9), # LMFDB
+ (0, (13,22), 32**9),
+ (0, (14,15), 85.96137**9),
+ (0, (16,), 22**9),
+ (0, (18,19,24), 50**9),
+ (0, (23,), 67**9),
+ (0, (25,), 28**9),
+ (0, (26,30), 35**9),
+ # r2=1 bound for arbitrary t is 27316369
+ (1, skip(1,34,{28,31}), None),
+ (1, (28,31), 15**9),
+ # r2=2 bound for arbitrary t is 27316369
+ (2, skip(1,28,{25})+(32,), None),
+ (2, (25,), 23**9),
+ (2, (28,30,31), 15**9),
+ (2, (29,), 18**9),
+ # r2=3 bound for arbitrary t is 146723910
+ (3, (1,2,3,5,6,7,9,10,11,14,15,17,21,23,25,27,30,32,33), None),
+ (3, (4,8,12,20,28,31), 15**9), # 8 is LMFDB
+ (3, (13,16), 12**9), # LMFDB
+ (3, (18,24), 16**9),
+ (3, (19,21,26), 20**9),
+ (3, (22,29), 18**9),
+ # r2=4 bound for arbitrary t is 39657561
+ (4, (1,2,4,6,7,12,13,17,20,22,25,28,29), None),
+ (4, (3,8,10,11,30,31), 15**9), # 8 from LMFDB
+ (4, (5,), 20**9), # LMFDB
+ (4, (14,15), 18**9), # LMFDB
+ (4, (16,), 12**9), # LMFDB
+ (4, (18,24), 16**9),
+ (4, (19,21,26), 20**9),
+ (4, (23,), 17**9)], # LMFDB
+ 10: [# bound for arbitrary r2,t is 190612177
+ (0, decic_with_quad, 12*10**10),
+ (0, decic_with_quint, 10**13),
+ (1, skip(1,45,{14,23,29,36,39,43}), None),
+ (1, (14,23,29,36,39), 10**13), # decic with quintic
+ (1, (43,), 12*10**10), # decic with quadratic
+ (2, skip(1,14,{8})+(17,18,19,20,26,30,31,32,35), None),
+ (2, decic_with_quad, 12*10**10),
+ (2, decic_with_quint, 10**12),
+ (3, skip(1,45,{13,14,23,29,32,35,36,38,39,43}), None),
+ (3, (14,23,29,36,38,39), 10**12), # decic with quintic
+ (3, (43,), 12*10**10), # decic with quadratic
+ (4, (1,2,6), None),
+ (4, decic_with_quad, 12*10**10),
+ (4, decic_with_quint, 10**12),
+ (5, (4,7,8,10,13,15,18,20,24,25,26,28,31,32,34,37,42,44), None),
+ (5, decic_with_quad, 12*10**10),
+ (5, decic_with_quint, 10**12)],
+ 11: [(1, tup(1,8), None),
+ (2, tup(1,7), None),
+ (3, tup(1,8), None),
+ (4, tup(1,5), None),
+ (5, (1,3,5,6,7), None)],
+ }
+
+ # nS[n] consists of specific sets S so that we have completeness in degree n for number fields unramified outside S.
+ self._nS = {
+ 5: {(2,191), (3,163), (3,181), (3,211), (3,241), (3,401), (3,431), (3,461), (5,211), (5,241), (7,163), (7,181), (2,7,31), (2,7,41), (2,11,31)},
+ 6: {(2,3,7)},
+ 7: {(2,11), (2,13), (3,11), (11,13)}
+ }
+
+ # If nSp[n][k] = M then we have completeness in degree n for number fields unramified outside of S for all S of size k consisting of primes less than M.
+ self._nSp = {
+ 2: {1: 12000, 2: 500, 3: 100, 4: 30, 5: 30, 6: 30, 7: 30, 8: 30, 9: 30, 10: 30},
+ #2: {1: 12000, 2: 500, 3: 100, 4: 30, 5: 18, 6: 18, 7: 18},
+ 3: {1: 12000, 2: 500, 3: 100, 4: 30, 5: 30, 6: 30, 7: 30, 8: 30, 9: 30, 10: 30},
+ #3: {1: 12000, 2: 500, 3: 100, 4: 30, 5: 24, 6: 24, 7: 24, 8: 24, 9: 24},
+ 4: {1: 12000, 2: 500, 3: 100, 4: 30, 5: 14, 6: 14},
+ 5: {1: 7500, 2: 150, 3: 24, 4: 8},
+ 6: {1: 2000, 2: 32},
+ 7: {1: 192, 2: 6},
+ }
+
+ # nSGp[n][k] is a list of pairs (M, Gs) so that we have completeness in degree n for number fields with Galois group nTt for t in Gs and unramified outside a set S of size k all of whose primes are less than M
+ self._nSGp = {
+ 6: {1: [(5000, tup(1,15))],
+ 2: [(100, (12,14))],
+ 3: [(12, (12,14)), (8, tup(1,15))],
+ 4: [(8, tup(1,15))]},
+ 7: {1: [(5000, (1,2,3)), (1500, (4,)), (228, (5,6))],
+ 2: [(42, (1,)), (14, (3,)), (8, (2,)), (6, (4,))],
+ 3: [(42, (1,)), (14, (3,)), (8, (2,)), (6, (4,))],
+ 4: [(42, (1,)), (14, (3,)), (8, (2,))],
+ 5: [(42, (1,)), (14, (3,))],
+ 6: [(42, (1,)), (14, (3,))],
+ 7: [(42, (1,))],
+ 8: [(42, (1,))],
+ 9: [(42, (1,))],
+ 10: [(42, (1,))],
+ 11: [(42, (1,))],
+ 12: [(42, (1,))],
+ 13: [(42, (1,))]},
+ 8: {1: [(2500, octic_2_group), (230, octwith4), (228, (37,)), (200, (25,)), (8, octic_with_quartic), (8, (25,36)), (6, (33,34,41,42,45,46,47))],
+ 2: [(250, octic_2_group), (8, octic_with_quartic), (8, (25,36)), (6, (33,34,41,42,45,46,47))],
+ 3: [(8, octic_with_quartic), (8, (25,36)), (6, (33,34,41,42,45,46,47))],
+ 4: [(8, (25,36))]},
+ 9: {1: [(6, tup(1,19)+tup(20,26)+(28,29,31)), (6, (19,26,30))],
+ 2: [(6, tup(1,19)+tup(20,26)+(28,29,31)), (6, (19,26,30))],
+ 3: [(6, tup(1,19)+tup(20,26)+(28,29,31))]},
+ 10: {1: [(20, (32,)), (6, decic_with_quint), (6, (6,7,9,10,13,17)), (4, (18,19,20,21,26,27,32,33))],
+ 2: [(20, (32,)), (6, decic_with_quint), (6, (6,7,9,10,13,17)), (4, (18,19,20,21,26,27,32,33))],
+ 3: [(6, decic_with_quint), (6, (6,7,9,10,13,17))]},
+ 11: {1: [(200, (1,)), (5000, (2,)), (12, (3,))],
+ 2: [(8, (1,2,3))],
+ 3: [(8, (1,2,3))],
+ 4: [(8, (1,2,3))]},
+ 13: {1: [(5000, (2,))]},
+ 24: {1: [(1000, (1,))]},
+ 25: {1: [(1000, (1,))]},
+ }
+
+ # nSGp1[n] is a list of triples (p, M, Gs) so that we have completeness in degree n for number fields with Galois group nTt for t in Gs and unramified outside {p,q} for q < M.
+ self._nSGp1 = {
+ 4: [(2, 2500, quartic_2_group)],
+ 5: [(3, 1328, (1,2,4)), (2, 980, (1,2,4))],
+ 8: [(2, 2500, octic_2_group), (2, 200, (25,))],
+ }
+
+ # nSG[n] is a list of pairs (T, Gs) so that we have completeness in degree n for number fields with Galois group nTt for t in Gs and unramified outside S for any subset S of T.
+ self._nSG = {
+ 5: [((2,3,7,11), (1,2,4)),
+ ((2,3,7,31), (1,2,4)),
+ ((2,3,11,19), (1,2,4)),
+ ((2,3,31), (1,2,4)),
+ ((2,3,37), (1,2,4)),
+ ((2,3,41), (1,2,4)),
+ ((2,3,43), (1,2,4)),
+ ((2,3,53), (1,2,4)),
+ ((2,3,61), (1,2,4)),
+ ((2,3,79), (1,2,4)),
+ ((2,3,89), (1,2,4)),
+ ((2,3,101), (1,2,4)),
+ ((2,3,103), (1,2,4)),
+ ((2,3,107), (1,2,4)),
+ ((2,3,113), (1,2,4)),
+ ((2,3,127), (1,2,4)),
+ ((2,3,131), (1,2,4)),
+ ((2,3,137), (1,2,4)),
+ ((2,3,151), (1,2,4)),
+ ((2,5,13), (1,2,4)),
+ ((2,5,17), (1,2,4)),
+ ((2,5,23), (1,2,4)),
+ ((2,5,29), (1,2,4)),
+ ((2,5,31), (1,2,4)),
+ ((2,7,17), (1,2,4)),
+ ((2,7,19), (1,2,4)),
+ ((2,7,59), (1,2,4)),
+ ((2,7,61), (1,2,4)),
+ ((2,7,71), (1,2,4)),
+ ((2,7,103), (1,2,4)),
+ ((2,7,127), (1,2,4)),
+ ((2,7,131), (1,2,4)),
+ ((2,13,71), (1,2,4)),
+ ((2,17,31), (1,2,4)),
+ ((2,19,23), (1,2,4)),
+ ((2,23,41), (1,2,4)),
+ ((2,29,31), (1,2,4)),
+ ((2,11,13), (1,2,4)),
+ ((2,11,17), (1,2,4)),
+ ((2,11,19), (1,2,4)),
+ ((2,11,23), (1,2,4)),
+ ((2,11,29), (1,2,4)),
+ ((2,11,31), (1,2,4)),
+ ((2,11,37), (1,2,4)),
+ ((2,11,41), (1,2,4)),
+ ((2,11,43), (1,2,4)),
+ ((2,11,47), (1,2,4)),
+ ((2,11,53), (1,2,4)),
+ ((2,11,59), (1,2,4)),
+ ((2,11,61), (1,2,4)),
+ ((2,11,67), (1,2,4)),
+ ((2,11,71), (1,2,4)),
+ ((2,11,73), (1,2,4)),
+ ((2,13,29), (1,2,4)),
+ ((2,13,31), (1,2,4)),
+ ((2,13,37), (1,2,4)),
+ ((2,13,41), (1,2,4)),
+ ((2,13,43), (1,2,4)),
+ ((2,13,47), (1,2,4)),
+ ((2,13,53), (1,2,4)),
+ ((2,13,59), (1,2,4)),
+ ((2,13,61), (1,2,4)),
+ ((2,13,67), (1,2,4)),
+ ((2,13,71), (1,2,4)),
+ ((2,13,73), (1,2,4)),
+ ((2,17,31), (1,2,4)),
+ ((2,19,23), (1,2,4)),
+ ((2,23,41), (1,2,4)),
+ ((2,29,31), (1,2,4)),
+ ((3,5,17), (1,2,4)),
+ ((3,5,31), (1,2,4)),
+ ((3,5,37), (1,2,4)),
+ ((3,7,11), (1,2,4)),
+ ((3,7,11,17), (1,2,4)),
+ ((3,7,17), (1,2,4)),
+ ((3,7,31), (1,2,4)),
+ ((3,7,41), (1,2,4)),
+ ((3,7,61), (1,2,4)),
+ ((3,7,101), (1,2,4)),
+ ((3,7,107), (1,2,4)),
+ ((3,7,131), (1,2,4)),
+ ((3,7,139), (1,2,4)),
+ ((3,7,163), (1,2,4)),
+ ((3,7,181), (1,2,4)),
+ ((3,11,13), (1,2,4)),
+ ((3,11,17), (1,2,4)),
+ ((3,11,19), (1,2,4)),
+ ((3,11,29), (1,2,4)),
+ ((3,11,31), (1,2,4)),
+ ((3,11,41), (1,2,4)),
+ ((3,11,61), (1,2,4)),
+ ((3,11,71), (1,2,4)),
+ ((3,11,73), (1,2,4)),
+ ((3,11,101), (1,2,4)),
+ ((3,11,103), (1,2,4)),
+ ((3,11,109), (1,2,4)),
+ ((3,13,31), (1,2,4)),
+ ((3,13,41), (1,2,4)),
+ ((3,13,61), (1,2,4)),
+ ((3,13,71), (1,2,4)),
+ ((3,13,89), (1,2,4)),
+ ((3,17,37), (1,2,4)),
+ ((3,17,41), (1,2,4)),
+ ((3,17,43), (1,2,4)),
+ ((3,17,71), (1,2,4)),
+ ((3,19,61), (1,2,4)),
+ ((3,29,37), (1,2,4)),
+ ((3,29,41), (1,2,4)),
+ ((3,31,41), (1,2,4)),
+ ((5,7,13), (1,2,4)),
+ ((5,7,17), (1,2,4)),
+ ((5,7,71), (1,2,4)),
+ ((5,7,97), (1,2,4)),
+ ((5,11,13), (1,2,4)),
+ ((5,11,19), (1,2,4)),
+ ((5,11,31), (1,2,4)),
+ ((5,11,41), (1,2,4)),
+ ((5,11,43), (1,2,4)),
+ ((5,11,71), (1,2,4)),
+ ((5,13,23), (1,2,4)),
+ ((5,151), (1,2,4)),
+ ((5,163), (1,2,4)),
+ ((5,223), (1,2,4)),
+ ((5,241), (1,2,4)),
+ ((5,367), (1,2,4)),
+ ((5,571), (1,2,4)),
+ ((5,631), (1,2,4)),
+ ((7,11,17), (1,2,4)),
+ ((7,11,23), (1,2,4)),
+ ((7,11,37), (1,2,4)),
+ ((7,13,31), (1,2,4)),
+ ((7,13,37), (1,2,4)),
+ ((7,13,41), (1,2,4)),
+ ((7,151), (1,2,4)),
+ ((7,163), (1,2,4)),
+ ((7,181), (1,2,4)),
+ ((7,191), (1,2,4)),
+ ((7,211), (1,2,4)),
+ ((7,241), (1,2,4)),
+ ((7,257), (1,2,4)),
+ ((7,281), (1,2,4)),
+ ((7,313), (1,2,4)),
+ ((7,331), (1,2,4)),
+ ((7,379), (1,2,4)),
+ ((7,401), (1,2,4)),
+ ((7,409), (1,2,4)),
+ ((7,421), (1,2,4)),
+ ((7,433), (1,2,4)),
+ ((7,431), (1,2,4)),
+ ((7,491), (1,2,4)),
+ ((7,541), (1,2,4)),
+ ((7,571), (1,2,4)),
+ ((11,13,23), (1,2,4)),
+ ((11,17,19), (1,2,4)),
+ ((11,151), (1,2,4)),
+ ((11,167), (1,2,4)),
+ ((11,179), (1,2,4)),
+ ((11,181), (1,2,4)),
+ ((11,191), (1,2,4)),
+ ((11,211), (1,2,4)),
+ ((11,251), (1,2,4)),
+ ((11,269), (1,2,4)),
+ ((11,263), (1,2,4)),
+ ((11,271), (1,2,4)),
+ ((11,281), (1,2,4)),
+ ((11,283), (1,2,4)),
+ ((11,311), (1,2,4)),
+ ((11,293), (1,2,4)),
+ ((11,307), (1,2,4)),
+ ((11,331), (1,2,4)),
+ ((11,331), (1,2,4)),
+ ((11,359), (1,2,4)),
+ ((13,191), (1,2,4)),
+ ((13,223), (1,2,4)),
+ ((13,211), (1,2,4)),
+ ((13,307), (1,2,4)),
+ ((17,227), (1,2,4)),
+ ((17,211), (1,2,4)),
+ ((19,191), (1,2,4)),
+ ((19,157), (1,2,4)),
+ ((19,181), (1,2,4)),
+ ((19,193), (1,2,4)),
+ ((23,151), (1,2,4)),
+ ((23,173), (1,2,4))],
+ 8: [((2,3), (37,48)),
+ ((2,5), (37,48)),
+ ((2,29), (25,)),
+ ((7,29), (25,)),
+ ((41, 241), octic_2_group)],
+ 9: [((3,7,13), (1,2,6,7,17))],
+ 10: [((2,3,7), (32,)),
+ ((2,7), decic_with_quad),
+ ((2,5), (19,20,21,26,32)),
+ ((3,5), (18,19,20,21,26,27,32,33))],
+ 11: [((2,11), (3,)),
+ ((3,11), (3,)),
+ ((7,11), (3,))],
+ }
+
+ def display_reason(self, reasons):
+ """
+ Convert a set of collected reasons into a single string to display.
+
+ INPUT:
+
+ - ``reasons`` -- a set of reasons, which are either a string or
+ a tuple of the form ``(n, r2, galt, ramps, D_bound, grd_bound)``,
+ where entries are None if they are not applicable.
+ """
+ # Current tuples created:
+ # (n, r2, None, None, M, None)
+ # (n, r2, Gs, None, M, None)
+ # (n, None, Gs, None, None, M)
+ # (n, None, None, S, None, None)
+ # (n, None, Gs, S, None, None)
+ # We group by None pattern
+ def describe(tups):
+ ans = []
+ if tups[0][0] is not None:
+ degs = [str(tup[0]) for tup in tups]
+ if len(set(degs)) == 1:
+ ans.append(f"degree {degs[0]}")
+ else:
+ ans.append(f"degree {','.join(degs)}")
+ if tups[0][1] is not None:
+ sigs = [f"[{tup[0]-2*tup[1]},{tup[1]}]" for tup in tups]
+ if len(set(sigs)) == 1:
+ ans.append(f"signature {sigs[0]}")
+ else:
+ ans.append(f"signature {','.join(sigs)}")
+ if tups[0][2] is not None:
+ ts = [f"({','.join(str(t) for t in tup[2])})" if len(tup[2]) > 1 else str(tup[2][0]) for tup in tups]
+ gals = [f"{tup[0]}T{tt}" for (tup, tt) in zip(tups, ts)]
+ if len(set(gals)) == 1:
+ ans.append(f"Galois group {gals[0]}")
+ else:
+ ans.append(f"Galois group {','.join(gals)}")
+ if tups[0][3] is not None:
+ rams = ['{'+','.join(str(p) for p in tup[3])+'}' for tup in tups]
+ if len(set(rams)) == 1:
+ ans.append(f"unramified outside {rams[0]}")
+ else:
+ ans.append(f"unramified outside {','.join()}")
+ if tups[0][4] is not None:
+ Dbounds = [str(tup[4]) for tup in tups]
+ if len(set(Dbounds)) == 1:
+ ans.append(f"absolute discriminant at most {Dbounds[0]}")
+ else:
+ ans.append(f"absolute discriminant at most {','.join(Dbounds)}")
+ if tups[0][5] is not None:
+ grd = [str(tup[5]) for tup in tups]
+ if len(set(grd)) == 1:
+ ans.append(f"Galois root discriminant at most {grd[0]}")
+ else:
+ ans.append(f"Galois root discriminant at most {','.join(grd)}")
+ return ", ".join(ans)
+ strings = []
+ by_pattern = defaultdict(list)
+ non_incomp = []
+ for reason in reasons:
+ if isinstance(reason, str):
+ strings.append(reason)
+ if not reason.startswith("incompatible conditions"):
+ non_incomp.append(reason)
+ else:
+ by_pattern[tuple(i for i in range(6) if reason[i] is None)].append(reason)
+ if len(non_incomp) + len(by_pattern) > 0:
+ strings = non_incomp
+ return "number fields with " + "; ".join(strings + [describe(V) for V in by_pattern.values()])
+
+ def clear_signatures(self, n, D, r2opts, reasons):
+ if 2 <= n < len(self._maxD):
+ m = infinity
+ for r2 in set(r2opts):
+ M = self._maxD[n][r2]
+ if D.bounded(M):
+ r2opts.remove(r2)
+ reasons.add((n, r2, None, None, M, None))
+ m = min(m, M)
+ if m is not infinity:
+ D = D.intersection(bottom(m + 1))
+ return D
+
+ def clear_r2G(self, n, D, r2opts, galt, reasons):
+ r2G = defaultdict(dict)
+ for (r2, Gs, M) in self._r2G.get(n, []):
+ if r2 in r2opts and D.bounded(M):
+ for t in galt.intersection(Gs):
+ r2G[t][r2] = (r2, Gs, M)
+ for t in galt.intersection(r2G):
+ if set(r2G[t]) == set(r2opts):
+ galt.remove(t)
+ for r2, Gs, M in r2G[t].values():
+ reasons.add((n, r2, Gs, None, M, None))
+
+ def clear_grd(self, n, grd, galt, reasons):
+ by_t = {}
+ for (Gs, M) in self._grd.get(n, []):
+ if grd.bounded(M):
+ for t in galt.intersection(Gs):
+ by_t[t] = (Gs, M)
+ for t in galt.intersection(by_t):
+ galt.remove(t)
+ Gs, M = by_t[t]
+ reasons.add((n, None, Gs, None, None, M))
+
+ def clear_S(self, n, S, nram, galt, reasons, update_galt=True):
+ """
+ When False, if update_galt is True, update galt to remove t that can be proven complete.
+ """
+ if galt is not None and not update_galt:
+ galt = set(galt)
+ if nram is None:
+ nram = len(S)
+
+ if S in self._nS.get(n, {}):
+ reasons.add((n, None, None, S, None, None))
+ return True
+
+ M = self._nSp.get(n, {}).get(nram)
+ if M is not None and all(p < M for p in S):
+ reasons.add((n, None, None, S, None, None))
+ return True
+
+ if galt is None:
+ return False
+
+ for (M, Gs) in self._nSGp.get(n, {}).get(nram, []):
+ if all(p < M for p in S):
+ I = galt.intersection(Gs)
+ if I:
+ reasons.add((n, None, Gs, S, None, None))
+ galt.difference_update(I)
+ if not galt:
+ return True
+
+ if len(S) == 2:
+ for (p0, M, Gs) in self._nSGp1.get(n, []):
+ if min(S) == p0 and max(S) < M:
+ I = galt.intersection(Gs)
+ if I:
+ reasons.add((n, None, Gs, S, None, None))
+ galt.difference_update(I)
+ if not galt:
+ return True
+
+ SS = set(S)
+ for T, Gs in self._nSG.get(n, []):
+ if SS.issubset(T):
+ I = galt.intersection(Gs)
+ if I:
+ reasons.add((n, None, Gs, S, None, None))
+ galt.difference_update(I)
+ if not galt:
+ return True
+
+ return False
+
+ def galt(self, n, gal, isgal, cyc, ab, solv):
+ pos_constraints = []
+ neg_constraints = []
+ if isinstance(gal, str) and gal.count("T") == 1:
+ N, t = gal.split("T")
+ if N == str(n) and t.isdigit():
+ pos_constraints.append({int(t)})
+ else:
+ return # incompatible constraints
+ elif isinstance(gal, dict) and list(gal) == ["$in"]:
+ galt = set()
+ n = str(n)
+ for G in gal["$in"]:
+ if not isinstance(G, str) or G.count("T") != 1:
+ raise ValueError
+ N, t = G.split("T")
+ if N == n and t.isdigit():
+ galt.add(int(t))
+ pos_constraints.append(galt)
+ elif gal is not None or n >= len(self._num_trans):
+ raise ValueError
+ if cyc == 1:
+ if n == 32:
+ pos_constraints.append({33})
+ else:
+ pos_constraints.append({1})
+ elif cyc == 0:
+ if n == 32:
+ neg_constraints.append({33})
+ else:
+ neg_constraints.append({1})
+ elif cyc is not None:
+ raise ValueError
+
+ if ab == 1:
+ pos_constraints.append(self._ab.get(n, {1}))
+ elif ab == 0:
+ neg_constraints.append(self._ab.get(n, {1}))
+ elif ab is not None:
+ raise ValueError
+
+ gal_set = self._ab.get(n, {1}).union(self._nab_gal.get(n, set()))
+ if isgal == 1:
+ pos_constraints.append(gal_set)
+ elif isgal == 0:
+ neg_constraints.append(gal_set)
+ elif isgal is not None:
+ raise ValueError
+
+ if solv == 1:
+ if n in self._nsolv:
+ neg_constraints.append(self._nsolv[n])
+ elif solv == 0:
+ if n in self._nsolv:
+ pos_constraints.append(self._nsolv[n])
+ elif solv is not None:
+ raise ValueError
+
+ if pos_constraints:
+ galt = pos_constraints[0]
+ for Gs in pos_constraints[1:]:
+ galt.intersection_update(Gs)
+ else:
+ galt = set(range(1, self._num_trans[n] + 1))
+ for Gs in neg_constraints:
+ galt.difference_update(Gs)
+ return galt
+
+ def rd_grd_ratio(self, n, galt):
+ if n < len(self._rdgrd) and max(galt) <= len(self._rdgrd[n]):
+ return max(1 / self._rdgrd[n][t - 1] for t in galt)
+
+ def get_S(self, ramps, radical):
+ S = None
+ if radical is not None:
+ if isinstance(radical, dict):
+ if not (list(radical) == ["$lte"] and isinstance(ramps, dict) and "$containedin" in ramps and prod(ramps["$containedin"]) == radical["$lte"]):
+ # Such constraints are not created by parsing code, so we give up
+ return
+ # Now we can just fall back on ramps parsing below
+ else:
+ S = set(p for p,e in factor(radical))
+ if ramps is not None:
+ if isinstance(ramps, dict):
+ if "$containedin" in ramps:
+ if S is not None:
+ if not S.issubset(ramps["$containedin"]):
+ # incompatible, so result is complete. We return an S that will be accepted for all n <= 11
+ return []
+ else:
+ S = ramps["$containedin"]
+ else:
+ # $notcontains and $contains do not yield finite S
+ return
+ if isinstance(ramps, list):
+ if S is not None:
+ if not S.issubset(ramps):
+ # incompatible, so result is complete. We return an S that will be accepted for all n <= 11
+ return []
+ else:
+ S = ramps
+ if S is not None:
+ return tuple(sorted(S))
+
+ def _initial(self, db, query, reasons):
+ """
+ Attempt to prove completeness without splitting on degree
+ """
+ D, rd, grd = IntegerSet(query.get("disc_abs")), NumberSet(query.get("rd")), NumberSet(query.get("grd"))
+ if D.bounded(1656109):
+ reasons.add("absolute discriminant at most 1656109")
+ return True, None
+ if rd.bounded(5.989):
+ reasons.add("root discriminant at most 5.989")
+ return True, None
+ if grd.bounded(5.989):
+ reasons.add("Galois root discriminant at most 5.989")
+ return True, None
+ # Can also guarantee completeness based on regulator bounds and non-CMness: see https://arxiv.org/pdf/2112.15268
+
+ # TODO: use Odlyzko bounds to get upper bound on degree
+ return False, None
+
+ def _one_n(self, db, query, reasons):
+ n = query.get("degree")
+ if n == 1:
+ reasons.add("degree 1")
+ return True, None
+ # For now, we just do not guarantee completeness for any degrees larger than 25.
+ # TODO: improve this using Minkowski and Odlyzko bounds (which give us guarantees that there are no number fields
+ if n > 25:
+ return False, None
+
+ r2, D, sign, rd, grd = IntegerSet(query.get("r2")), IntegerSet(query.get("disc_abs")), query.get("disc_sign"), NumberSet(query.get("rd")), NumberSet(query.get("grd"))
+ r2opts = list(r2.intersection(IntegerSet([0, n//2])))
+ if sign == 1:
+ r2opts = [r2 for r2 in r2opts if r2 % 2 == 0]
+ elif sign == -1:
+ r2opts = [r2 for r2 in r2opts if r2 % 2 == 1]
+ if not r2opts:
+ reasons.add("incompatible conditions: signature and discriminant")
+ return True, None
+ if n == 2 and r2opts == [1]:
+ # Imaginary quadratic fields, where we can use Mark Watkins' paper (Class groups of imaginary quadratic fields) to guarantee completeness based on class number
+ h = query.get("class_number")
+ C = query.get("class_group")
+ if isinstance(C, list) and h is None:
+ h = prod(C)
+ h = IntegerSet(h)
+ if h.is_subset(top(97).union(IntegerSet([99,100]))):
+ # Class number 98 has entries slightly outside our bounds
+ # Watkins' result is actually unconditional
+ reasons.add("signature [0,1], class number at most 100 (except 98)")
+ return True, None
+
+ # We can also use https://msp.org/obs/2019/2-1/obs-v2-n1-p16-s.pdf
+ if isinstance(C, list) and all(c == 2 for c in C):
+ reasons.add("signature [0,1], class group of exponent 2")
+ return True, "depends on GRH"
+
+ if n > 2 and any(col in query for col in ["class_group", "class_number", "narrow_class_group", "narrow_class_number"]):
+ caveat = "depends on GRH"
+ else:
+ caveat = None
+
+ ## Completeness 1: degree, signature, discriminant ##
+ if grd.restricted():
+ rd = rd.pow_cap(grd, 1)
+ if not rd:
+ reasons.add("incompatible conditions: root discriminant and Galois root discriminant")
+ return True, None
+ if rd.restricted():
+ D = D.pow_cap(rd, n)
+ if not D:
+ reasons.add("incompatible conditions: root discriminant and discriminant")
+ return True, None
+ if D.restricted():
+ D = self.clear_signatures(n, D, r2opts, reasons)
+ if not r2opts:
+ return True, caveat
+
+ ## Completeness 2: degree, signature, Galois group, discriminant
+ gal, isgal, cyc, ab, solv, = query.get("galois_label"), query.get("is_galois"), query.get("gal_is_cyclic"), query.get("gal_is_abelian"), query.get("gal_is_solvable")
+ if gal or isgal or cyc or ab or solv:
+ try:
+ galt = self.galt(n, gal, isgal, cyc, ab, solv)
+ except ValueError:
+ # Parsing problem
+ return False, None
+ if not galt:
+ reasons.add("incompatible conditions: Galois group")
+ return True, None
+ if D.restricted():
+ self.clear_r2G(n, D, r2opts, galt, reasons)
+ if not galt:
+ return True, caveat
+
+ ## Completeness 3: degree, Galois group, Galois root discriminant ##
+ if D.restricted():
+ rd = rd.pow_cap(D, 1/n)
+ if not rd:
+ reasons.add("incompatible conditions: root discriminant and discriminant")
+ return True, None
+ if rd.restricted():
+ ratio = self.rd_grd_ratio(n, galt)
+ if ratio is not None:
+ grd = grd.pow_cap(rd, ratio)
+ if not grd:
+ reasons.add("incompatible conditions: root discriminant and Galois root discriminant")
+ return True, None
+ if grd.restricted():
+ self.clear_grd(n, grd, galt, reasons)
+ if not galt:
+ return True, caveat
+ else:
+ galt = None
+
+ ## Completeness 4: degree, ramified primes, and Galois group (optional)
+ # Can fill rams from discriminant range, or from radical
+ ramps, radical, nram = query.get("ramps"), query.get("disc_rad"), query.get("num_ram")
+ if isinstance(nram, dict):
+ if "$lte" in nram:
+ nram = nram["$lte"]
+ else:
+ # nram is complicated, so we give up on using it
+ nram = None
+ if ramps or radical:
+ S = self.get_S(ramps, radical)
+ if S == []: # incompatible
+ reasons.add("incompatible conditions: ramps and radical")
+ return True, None
+ if S is not None and self.clear_S(n, S, nram, galt, reasons):
+ return True, caveat
+
+ # Can also iterate over valid discriminants in a discriminant range
+ if D.restricted():
+ for S in D.stickelberger(n, r2opts):
+ if not self.clear_S(n, S, nram, galt, reasons, update_galt=False):
+ break
+ else:
+ if not reasons:
+ reasons.add("incompatible conditions: no valid discriminants in range")
+ return True, caveat
+
+ # Minkowski bound (only relevant for n>12)
+ if n >= len(self._maxD):
+ mbound = (3.14159265358979/4)**n * n**(2*n) / factorial(n)**2
+ if D.bounded(mbound):
+ reasons.add(f"number fields of degree {n} with discriminant at most {floor(mbound)} (Minkowski)")
+ return True, None
+ # TODO: Odlyzko bounds
+
+ return False, None
+
+ def __call__(self, db, query):
+ n = query.get("degree")
+
+ # We collect reasons that have contributed to completeness
+ # These have the following format:
+ # (n, r2, galt, ramps, D_bound, grd_bound)
+ # Where entries can be None if they are not applicable
+ reasons = set()
+ # First check completeness without splitting on degree
+ # This may also add an upper bound on degree, based on other inputs
+ done, caveat = self._initial(db, query, reasons)
+ if done:
+ return True, self.display_reason(reasons), caveat
+ if isinstance(n, dict):
+ nopts = IntegerSet(n).intersection(bottom(1))
+ if nopts.max() >= 48:
+ return False, None, None
+ caveats = set()
+ # Reverse order since we're less likely to have completeness in higher degree
+ for n in reversed(list(nopts)):
+ nquery = dict(query)
+ nquery["degree"] = n
+ ok, caveat = self._one_n(db, nquery, reasons)
+ if not ok:
+ return False, None, None
+ if caveat:
+ caveats.add(caveat)
+ if caveats:
+ caveats = ", ".join(caveats)
+ else:
+ caveats = None
+ else:
+ ok, caveats = self._one_n(db, query, reasons)
+ if not ok:
+ return False, None, None
+ return True, self.display_reason(reasons), caveats
+
+
+#### Artin representations ####
+
+minimal_label = {
+ '2T1': '2T1',
+ '3T1': '3T1',
+ '3T2': '3T2',
+ '6T2': '3T2',
+ '4T1': '4T1',
+ '4T3': '4T3',
+ '8T4': '4T3',
+ '4T4': '4T4',
+ '6T4': '4T4',
+ '12T4': '4T4',
+ '4T5': '4T5',
+ '6T7': '4T5',
+ '6T8': '4T5',
+ '8T14': '4T5',
+ '12T8': '4T5',
+ '12T9': '4T5',
+ '24T10': '4T5',
+ '5T1': '5T1',
+ '5T2': '5T2',
+ '10T2': '5T2',
+ '5T3': '5T3',
+ '10T4': '5T3',
+ '20T5': '5T3',
+ '5T4': '5T4',
+ '6T12': '5T4',
+ '10T7': '5T4',
+ '12T33': '5T4',
+ '15T5': '5T4',
+ '20T15': '5T4',
+ '30T9': '5T4',
+ '5T5': '5T5',
+ '6T14': '5T5',
+ '10T12': '5T5',
+ '10T13': '5T5',
+ '12T74': '5T5',
+ '15T10': '5T5',
+ '20T30': '5T5',
+ '20T32': '5T5',
+ '20T35': '5T5',
+ '24T202': '5T5',
+ '30T22': '5T5',
+ '30T25': '5T5',
+ '30T27': '5T5',
+ '40T62': '5T5',
+ '6T1': '6T1',
+ '6T3': '6T3',
+ '12T3': '6T3',
+ '6T5': '6T5',
+ '9T4': '6T5',
+ '18T3': '6T5',
+ '6T6': '6T6',
+ '8T13': '6T6',
+ '12T6': '6T6',
+ '12T7': '6T6',
+ '24T9': '6T6',
+ '6T9': '6T9',
+ '9T8': '6T9',
+ '12T16': '6T9',
+ '18T9': '6T9',
+ '18T11': '6T9',
+ '36T13': '6T9',
+ '6T10': '6T10',
+ '9T9': '6T10',
+ '12T17': '6T10',
+ '18T10': '6T10',
+ '36T14': '6T10',
+ '6T11': '6T11',
+ '8T24': '6T11',
+ '12T21': '6T11',
+ '12T22': '6T11',
+ '12T23': '6T11',
+ '12T24': '6T11',
+ '16T61': '6T11',
+ '24T46': '6T11',
+ '24T47': '6T11',
+ '24T48': '6T11',
+ '6T13': '6T13',
+ '9T16': '6T13',
+ '12T34': '6T13',
+ '12T35': '6T13',
+ '12T36': '6T13',
+ '18T34': '6T13',
+ '18T36': '6T13',
+ '24T72': '6T13',
+ '36T53': '6T13',
+ '36T54': '6T13',
+ '6T15': '6T15',
+ '10T26': '6T15',
+ '15T20': '6T15',
+ '20T89': '6T15',
+ '30T88': '6T15',
+ '36T555': '6T15',
+ '40T304': '6T15',
+ '45T49': '6T15',
+ '6T16': '6T16',
+ '10T32': '6T16',
+ '12T183': '6T16',
+ '15T28': '6T16',
+ '20T145': '6T16',
+ '20T149': '6T16',
+ '30T164': '6T16',
+ '30T166': '6T16',
+ '30T176': '6T16',
+ '36T1252': '6T16',
+ '40T589': '6T16',
+ '40T592': '6T16',
+ '45T96': '6T16',
+ '7T1': '7T1',
+ '7T2': '7T2',
+ '14T2': '7T2',
+ '7T3': '7T3',
+ '21T2': '7T3',
+ '8T1': '8T1',
+ '8T5': '8T5',
+ '9T1': '9T1'}
+class ArtinBound(ColTest):
+ def __call__(self, db, query):
+ group, dim, container, N = query.get("GaloisLabel"), query.get("Dim"), query.get("Container"), IntegerSet(query.get("Conductor"))
+ if isinstance(group, dict) and "$in" in group:
+ # Artin reps are stored using the minimal transitive rep for the group
+ groups = set(minimal_label.get(G) for G in group["$in"])
+ if None in groups:
+ return False, None, None
+ elif group not in minimal_label:
+ return False, None, None
+ else:
+ groups = [minimal_label[group]]
+ if isinstance(dim, dict) or isinstance(container, dict):
+ # This could be improved, but for simplicity we just don't guarantee completeness in this case
+ return False, None, None
+ bounds = {
+ "2T1": [(1, "2t1", 10000)], # C2
+ "3T1": [(1, "3t1", 11180)], # C3
+ "4T1": [(1, "4t1", 796)], # C4
+ "5T1": [(1, "5t1", 752)], # C5
+ "6T1": [(1, "6t1", 577)], # C6
+ "3T2": [(2, "3t2", 177662241)], # S3
+ "7T1": [(1, "7t1", 483)], # C7
+ "8T1": [(1, "8t1", 249)], # C8
+ "4T3": [(2, "4t3", 22500)], # D4
+ "8T5": [(2, "8t5", 215444)], # Q8
+ "9T1": [(1, "9t1", 387)], # C9
+ "5T2": [(2, "5t2", 40000)], # D5
+ "4T4": [(3, "4t4", 3375000)], # A4
+ "6T3": [(2, "6t3", 22500)], # D6
+ "7T2": [(2, "7t2", 40000)], # D7
+ "6T5": [(2, "6t5", 2828)], # C3 x S3
+ "5T3": [(4, "5t3", 1600000000)], # F5
+ "7T3": [(3, "7t3", 1000000)], # C7:C3
+ "4T5": [(3, "4t5", 22497), (3, "6t8", 635168)], # S4
+ "6T6": [(3, "6t6", 22497)], # A4 x C2
+ "6T10": [(4, "6t10", 3374494)], # C3^2:C4
+ "6T9": [(4, "6t9", 7998219)], # S3^2
+ "6T11": [(3, "6t11", 22497)], # S4 x C2
+ "5T4": [(3, "12t33", 66627), (4, "5t4", 613778), (5, "6t12", 52222435)], # A5
+ "6T13": [(4, "6t13", 22500), (4, "12t34", 3375000)], # SO+(4,2)
+ "5T5": [(4, "5t5", 7225), (4, "10t12", 614125), (5, "6t14", 52200625), (5, "10t13", 52200625), (6, "20t30", 4437053125)], # S5
+ "6T15": [(5, "6t15", 287296), (8, "36t1252", 23534089616228), (9, "10t26", 174981250375214), (10, "30t88", 10077696000000000)], # A6
+ "6T16": [(5, "6t16", 3600), (5, "12t183", 216000), (9, "10t32", 52753592444), (9, "20t145", 174981250375214), (10, "30t176", 167961600000000), (16, "36t1252", 553853374064674583164228907)], # S6
+ }
+ reasons = set()
+ for group in groups:
+ if group in bounds:
+ L = bounds[group]
+ for i, (d, P, M) in enumerate(L):
+ if dim in [d, None] and container in [P, None]:
+ if N.bounded(M):
+ if i == 0:
+ dimcon = ""
+ elif all(trip[0] < d for trip in L[:i]):
+ dimcon = f" dimension {d},"
+ else:
+ dimcon = f" dimension {d}, container {P},"
+ reasons.add(f"group {group},{dimcon} and conductor at most {M}")
+ break
+ else:
+ return False, None, None
+ else:
+ dimcon = [f"group {group}"]
+ if dim is not None:
+ dimcon.append(f"dimension {dim}")
+ if container is not None:
+ dimcon.append(f"container {container}")
+ dimcon = display_opts(dimcon, "and")
+ reasons.add(f"{dimcon} (no such Artin representations)")
+ return True, "Artin representations with " + "; ".join(sorted(reasons)), None
+
+
+#### Finite groups ####
+
+class GroupBound(ColTest):
+ def __call__(self, db, query):
+ N, td, pd, Qd, perfect, simple, abelian = IntegerSet(query.get("order")), IntegerSet(query.get("transitive_degree")), IntegerSet(query.get("permutation_degree")), IntegerSet(query.get("linQ_dim")), query.get("perfect"), query.get("simple"), query.get("abelian")
+ # First missing
+ # PSL(2,2729) = 10162031880
+ #POmega-(4,53)= 11082179160
+ # PSU(3,23) = 26056457856
+ # 2B(2,128) = 34093383680
+ # Suz = 448345497600
+ # ON = 460815505920
+ # G(2,7) = 664376138496
+ # PSL(4,7) = 2317591180800
+ # PSp(4,23) = 20674026236160
+ #POmega+(10,2)= 23499295948800
+ # PSU(5,4) = 53443952640000
+ # PSU(4,9) = 101798586432000
+ # PSp(10,2) = 24815256521932800
+ # 2G(2,243) = 49825657439340552
+ #POmega+(8,4) = 67010895544320000
+ #POmega-(8,4) = 67536471195648000
+ # 3D(4,4) = 67802350642790400
+ # PSp(6,7) = 273457218604953600
+ # PSL(8,2) = 5348063769211699200
+ #POmega-(12,2)= 51615733565620224000
+ # PSL(5,7) = 187035198320488089600
+ # PSL(6,4) = 361310134959341568000
+ #POmega-(10,3)= 650084965259666227200
+ # PSU(6,4) = 1120527288631296000000
+ # Omega(9,4) = 4408780839651901440000
+ # PSp(8,4) = 4408780839651901440000
+ # PSU(7,3) = 72853912155490594652160
+ # 2F(4,8) = 264905352699586176614400
+ # PSU(9,2) = 325473292721108444774400
+ # F(4,3) = 5734420792816671844761600
+ # PSL(7,4) = 72736898347485916060188672000
+ # PSU(8,3) = 261303669649855006027009228800
+ ord2000 = IntegerSet({"$lte": 2000, "$nin":[512,640,768,896,1024,1152,1280,1408,1536,1664,1792,1920]})
+ if N.is_subset(ord2000):
+ return True, "groups of order at most 2000 except orders larger than 500 that are multiples of 128", None
+ if perfect is True and N.bounded(50000):
+ return True, "perfect groups of order at most 50000", None
+ if simple is True and abelian is False and N.bounded(10162031879):
+ return True, "nonabelian simple groups of order less than 10162031880", None
+ td48 = IntegerSet(top(31)).union(IntegerSet([33,47]))
+ if td.is_subset(td48):
+ return True, "groups with minimal transitive degree at most 47 (except 32)", None
+ if td.bounded(47) and N.bounded(40000000000, infinity):
+ return True, "groups with minimal transitive degree 32 and order at least 40 billion", None
+ if pd.bounded(15):
+ return True, "groups with minimal permutation degree at most 15", None
+ if Qd.bounded(6):
+ return True, r"groups with linear $\Q$-degree at most 6", None
+ return False, None, None
+
+
+##################################
+# Specific completeness checkers #
+##################################
+
+CompletenessChecker("lfunc_search", [
+ (("degree", "rational", "conductor"), CBound(1, True, 2800), "L-functions with degree 1 and conductor at most 2800"),
+ ])
+
+
+CompletenessChecker("mf_newforms", [
+ ("Nk2", Bound(4000), "newforms with $Nk^2$ at most 4000"),
+ (("char_order", "Nk2"), CBound(1, 40000), "newforms with trivial character and $Nk^2$ at most 40000"),
+ (("level", "Nk2"), Bound(24, 40000), "newforms with level $N$ at most 24 and $Nk^2$ at most 40000"),
+ (("level", "Nk2"), Bound(10, 100000), "newforms with level $N$ at most 10 and $Nk^2$ at most 100000"),
+ (("level", "weight"), Bound(100, 12), "newforms with level at most 100 and weight at most 12"),
+ # k > 1, dim S_k^new(N,chi) <= 100, Nk2 <= 40000
+ (("weight", "char_order", "level"), CBound(2, 1, 50000), "newforms with trivial character, weight 2, and level at most 50000"),
+ (("weight", "char_order", "level"), CPrimeBound(2, 1, 1000000), "newforms with trivial character, weight 2 and prime level at most a million")],
+ fill=[CMFFiller()])
+
+
+CompletenessChecker("maass_rigor", [(("level", "spectral_parameter"), MaassBound())])
+
+
+CompletenessChecker("hmf_forms", [(("level_norm"), HMFBound())])
+
+
+CompletenessChecker("bmf_forms", [("level_norm", BianchiBound(ec=False))], fill=[FieldLabelFiller(False)])
+
+
+# No completeness guarantees for Siegel modular forms
+
+
+CompletenessChecker("ec_curvedata", [
+ ("conductor", Bound(500000), "elliptic curves with conductor at most 500000"),
+ ("conductor", PrimeBound(300000000), "elliptic curves with prime conductor at most 300 million"),
+ ("conductor", Smooth(10), "elliptic curves with 7-smooth conductor"),
+ ("absD", Bound(500000), "elliptic curves with minimal discriminant at most 500000")])
+
+
+CompletenessChecker("ec_nfcurves", [("conductor_norm", BianchiBound(ec=True))], fill=[FieldLabelFiller(True)])
+
+
+# No completeness guarantees for genus 2 curves
+
+
+# Skip modular curves for the moment
+
+
+CompletenessChecker("hgcwa_passports", [
+ ("genus", Bound([2, 4]), "groups acting as automorphisms of curves of genus 2, 3 or 4"),
+ (("genus", "g0"), Bound([2, 15], 0), "groups G acting as automorphisms of curves X with the genus of X at most 15 and the genus of X/G equal to 0")])
+
+
+CompletenessChecker("av_fq_isog", [
+ (("g", "q"), Bound(1, RealSet((-infinity, 503), [510, 520], [620, 630], [728,732], [1022,1030])), "isogeny classes of elliptic curves over fields of cardinality less than 500 or 512, 625, 729, 1024"),
+ (("g", "q"), Bound(2, RealSet((-infinity, 223), [242, 250], [252, 256], [338, 346], [510, 520], [620, 630], [728,732], [1022,1030])), "isogeny classes of abelian varieties of dimension at most 2 over fields of cardinality at most 211 or 243, 256, 343, 512, 625, 729, 1024"),
+ (("g", "q"), Bound(3, 25), "isogeny classes of abelian varieties of dimension at most 3 over fields of cardinality at most 25"),
+ (("g", "q"), Bound(4, 5), "isogeny classes of abelian varieties of dimension at most 4 over fields of cardinality at most 5"),
+ (("g", "q"), Bound(5, 3), "isogeny classes of abelian varieties of dimension at most 5 over GF(2) and GF(3)"),
+ (("g", "q"), Bound(6, 2), "isogeny classes of abelian varieties of dimension at most 6 over GF(2)")],
+ fill=[SumFiller("g", "p_rank", "p_rank_deficit"),
+ SumFiller("g", "angle_rank", "angle_corank")])
+
+
+CompletenessChecker("belyi_galmaps", [("deg", Bound(6), "Belyi maps of degree at most 6")])
+
+
+CompletenessChecker("nf_fields", [((), NFBound())])
+
+
+CompletenessChecker("lf_fields", [(("n", "p"), Bound(23, 199), "p-adic fields of degree at most 23 and residue characteristic at most 199")], fill=[MulFiller("n", "e", "f")])
+
+
+CompletenessChecker("lf_families", [(("n0", "n", "p"), Bound(1, 47, 199), "families of p-adic fields of degree at most 47 and residue characteristic at most 199"),
+ (("n0", "n_absolute", "p"), Bound(15, 47, 199), "families of p-adic extensions with absolute degree at most 47, base degree at most 15 and residue characteristic at most 199")],
+ fill=[MulFiller("e_absolute", "e0", "e", backfill=True),
+ MulFiller("f_absolute", "f0", "f", backfill=True),
+ MulFiller("n", "e", "f"),
+ MulFiller("n0", "e0", "f0"),
+ MulFiller("n_absolute", "n0", "n", backfill=True),
+ MulFiller("n_absolute", "e_absolute", "f_absolute")])
+
+
+CompletenessChecker("char_dirichlet", [("modulus", Bound(1000000), "Dirichlet characters with modulus at most a million")])
+
+
+CompletenessChecker("artin_reps", [(("GaloisLabel", "Conductor"), ArtinBound())])
+
+
+CompletenessChecker("hgm_families", [("degree", Bound(7), "hypergeometric families with degree at most 7")])
+
+
+CompletenessChecker("gps_transitive", [
+ ("n", Bound(RealSet((-infinity, 32), [33, 47])), "transitive groups of degree at most 47 (except 32)"),
+ (("n", "order"), Bound(47, 511), "transitive groups of degree 32 and order at most 511"),
+ (("n", "order"), Bound(47, (39999999999, infinity)), "transitive groups of degree at most 47 and order at least 40 billion")])
+
+
+CompletenessChecker("gps_st", [
+ (("rational", "weight", "degree"), CBound(True, 1, 6), "rational Sato-Tate groups of weight at most 1 and degree at most 6"),
+ (("rational", "weight", "degree"), Specific([True], [0], [1]), "rational Sato-Tate groups of weight 0 and degree 1"),
+ (("weight", "degree", "components"), CBound(0, 1, 10000), "Sato-Tate groups of weight 0, degree 1 and at most 10000 components")])
+
+
+CompletenessChecker("gps_groups", [((), GroupBound())], null_override=["transitive_degree", "permutation_degree", "linQ_dim"])
+
+
+# Nothing for lat_lattices
diff --git a/lmfdb/utils/search_columns.py b/lmfdb/utils/search_columns.py
index 0fde9c6acb..eec9333136 100644
--- a/lmfdb/utils/search_columns.py
+++ b/lmfdb/utils/search_columns.py
@@ -573,6 +573,9 @@ def eval_rational_list(s):
- once-nested lists like "[[1,2],[3,4]]" or "1,2;3,4"
- single quotes wrapping the integers/rationals, like "['1','2','3']"
"""
+ if s is None:
+ return
+
def split(x):
if not x:
return []
diff --git a/lmfdb/utils/search_wrapper.py b/lmfdb/utils/search_wrapper.py
index de6539cca3..14438d0561 100644
--- a/lmfdb/utils/search_wrapper.py
+++ b/lmfdb/utils/search_wrapper.py
@@ -7,7 +7,8 @@
from lmfdb.app import ctx_proc_userdata, is_debug_mode
from lmfdb.utils.search_parsing import parse_start, parse_count, SearchParsingError
-from lmfdb.utils.utilities import flash_error, flash_info, to_dict
+from lmfdb.utils.utilities import flash_error, flash_info, flash_success, to_dict
+from lmfdb.utils.completeness import results_complete
def use_split_ors(info, query, split_ors, offset, table):
@@ -270,7 +271,10 @@ def __call__(self, info):
# Display warning message if user searched on column(s) with null values
if query:
nulls = table.stats.null_counts()
- if nulls:
+ complete, msg, caveat = results_complete(table.search_table, query, table._db, info.get("search_array"))
+ if complete:
+ flash_success("The results below are complete, since the LMFDB contains all " + msg)
+ elif nulls: # TODO: We already run a version of this inside results_complete. Should be combined
search_columns = table._columns_searched(query)
nulls = {col: cnt for col, cnt in nulls.items() if col in search_columns}
col_display = {}
@@ -295,6 +299,8 @@ def __call__(self, info):
msg = 'Search results may be incomplete due to uncomputed quantities: '
msg += ", ".join(nulls.values())
flash_info(msg)
+ if caveat:
+ flash_info("The completeness " + caveat)
return render_template(template, info=info, title=title, **template_kwds)
diff --git a/lmfdb/utils/utilities.py b/lmfdb/utils/utilities.py
index 58ce58315c..d245466713 100644
--- a/lmfdb/utils/utilities.py
+++ b/lmfdb/utils/utilities.py
@@ -789,6 +789,10 @@ def flash_info(errmsg, *args):
""" flash information in grey with args in black; warning may contain markup, including latex math mode"""
flash(Markup("Note: " + (errmsg % tuple("%s" % escape(x) for x in args))), "info")
+def flash_success(msg, *args):
+ """ flash information in green with args in black; msg may contain markup, including latex math mode"""
+ flash(Markup(msg % tuple("%s" % escape(x) for x in args)), "success")
+
################################################################################
# Ajax utilities