diff --git a/lmfdb/abvar/fq/isog_class.py b/lmfdb/abvar/fq/isog_class.py
index 4a93cbca8f..361e28e4d0 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..8cf5ac3024 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],
@@ -536,6 +562,7 @@ def short_label(d):
[count],
]
+
def search_types(self, info):
return self._search_again(info, [
('List', 'List of isogeny classes'),
@@ -552,6 +579,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 +589,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 +716,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..95dd68cec2 100644
--- a/lmfdb/abvar/fq/test_av.py
+++ b/lmfdb/abvar/fq/test_av.py
@@ -157,3 +157,12 @@ 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..07785af791 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,54 @@ 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<",
+ )
+
+
+