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<", + ) + + +