diff --git a/lmfdb/genus2_curves/code.yaml b/lmfdb/genus2_curves/code.yaml index 080c692faf..5949ce7fe7 100644 --- a/lmfdb/genus2_curves/code.yaml +++ b/lmfdb/genus2_curves/code.yaml @@ -8,21 +8,25 @@ frontmatter: {lang} code for working with genus 2 curve {label}. curve: - comment: Define the curve + comment: Define the curve (minimal equation) magma: | R := PolynomialRing(Rationals()); fh := %s; f := R![a : a in fh[1]]; h := R![a : a in fh[2]]; - C := HyperellipticCurve(f, h); + Cmin := HyperellipticCurve(f, h); + +simple_curve: + comment: Simplified equation + magma: Csim := SimplifiedModel(Cmin); aut: comment: Automorphism group - magma: AutomorphismGroup(C); + magma: AutomorphismGroup(Cmin); jacobian: comment: Jacobian - magma: J := Jacobian(SimplifiedModel(C)); + magma: J := Jacobian(Csim); tors: comment: Torsion subgroup @@ -30,11 +34,11 @@ tors: cond: comment: Conductor - magma: Conductor(LSeries(C)); + magma: Conductor(LSeries(Cmin%s)); disc: comment: Discriminant - magma: Discriminant(C); + magma: Discriminant(Cmin); ntors: comment: Torsion order of Jacobian diff --git a/lmfdb/genus2_curves/main.py b/lmfdb/genus2_curves/main.py index 4e22801dc2..be1df47db2 100644 --- a/lmfdb/genus2_curves/main.py +++ b/lmfdb/genus2_curves/main.py @@ -926,7 +926,7 @@ def labels_page(): learnmore=learnmore_list_remove("labels"), ) -sorted_code_names = ['curve', 'aut', 'jacobian', 'tors', 'cond', 'disc', 'ntors', 'mwgroup'] +sorted_code_names = ['curve', 'simple_curve', 'aut', 'jacobian', 'tors', 'cond', 'disc', 'ntors', 'mwgroup'] Comment = {'magma': '//', 'sage': '#', 'gp': '\\\\', 'pari': '\\\\'} diff --git a/lmfdb/genus2_curves/test_genus2_curves.py b/lmfdb/genus2_curves/test_genus2_curves.py index cd5148d159..bca03aec87 100644 --- a/lmfdb/genus2_curves/test_genus2_curves.py +++ b/lmfdb/genus2_curves/test_genus2_curves.py @@ -1,4 +1,11 @@ +import re + +from sage.all import PolynomialRing, QQ + from lmfdb.tests import LmfdbTest +from lmfdb.genus2_curves.web_g2c import (comp_poly, simplify_hyperelliptic, + simplify_hyperelliptic_point, + simplify_hyperelliptic_scale) class Genus2Test(LmfdbTest): @@ -122,6 +129,23 @@ def test_download(self): self.tc.get("/Genus2Curve/Q/?query={'abs_disc':3976}&download=sage") self.tc.get("/Genus2Curve/Q/?query={'abs_disc':3976}&download=magma") + def test_code_download_conductor(self): + # Magma will not use Ogg's formula once v_2(disc) >= 12, so for those + # curves the conductor snippet has to hand it the L-factor at 2. The + # downloaded code needs the same treatment as the snippet on the page. + excfactors = "ExcFactors:=[*<2,Valuation(400,2),R![1]>*]" + page = self.tc.get("/Genus2Curve/Q/400/a/409600/1").get_data(as_text=True) + # the snippets on the page escape the angle brackets of the tuple + assert excfactors.replace("<", "<").replace(">", ">") in page + code = self.tc.get( + "/Genus2Curve/Q/400/a/409600/1/download/magma").get_data(as_text=True) + assert "Conductor(LSeries(Cmin: %s));" % excfactors in code + # curves with v_2(disc) < 12 get the plain call + code = self.tc.get( + "/Genus2Curve/Q/961/a/961/1/download/magma").get_data(as_text=True) + assert "Conductor(LSeries(Cmin));" in code + assert "ExcFactors" not in code + def test_rational_weierstrass_points_search(self): L = self.tc.get("/Genus2Curve/Q/?num_rat_wpts=4") assert "360.a.6480.1" in L.get_data(as_text=True) @@ -238,6 +262,64 @@ def test_ratpts(self): assert "rational points are known" in L.get_data(as_text=True) assert "for this curve" in L.get_data(as_text=True) + def test_simplified_model_points(self): + # 400.a.409600.1 has h = 0 and content(4f+h^2) = 4, so y-coordinates are + # divided by sqrt(4) = 2 and not by the content 4: the simplified equation + # equals the minimal one here, and the points must come through unchanged. + fh = [[1, 0, 4, 0, 4, 0, 1], []] + assert simplify_hyperelliptic_scale(fh) == 2 + R = PolynomialRing(QQ, ['x', 'z']) + x, z = R.gens() + g = sum(c*x**i*z**(6-i) for i, c in enumerate(simplify_hyperelliptic(fh))) + pts = [[1, -1, 0], [1, 1, 0], [0, -1, 1], [0, 1, 1]] + for pt in pts: + X, Y, Z = simplify_hyperelliptic_point(fh, pt) + assert [X, Y, Z] == pt + assert Y**2 == g(X, Z) + L = self.tc.get("/Genus2Curve/Q/400/a/409600/1") + page = L.get_data(as_text=True) + # the simplified-model snippet builds its points on Csim, and they are the + # points above rather than the halved ones the content used to produce + for pt in pts: + assert "Csim![%s,%s,%s]" % tuple(pt) in page + assert "(0 : -1/2 : 1)" not in page + # nor does the simplified Mordell-Weil table pick up a factor of 1/2 + assert "1/2z^3" not in page + + def test_simplified_model_mw_gens(self): + # 336.a.172032.1 is y^2 + (x^3 + xz^2)y = -x^6 + 15x^4z^2 - 75x^2z^4 - 56z^6 + # with a generator recorded as 3x^2 - 32z^2 = 0, 6y = -35xz^2. Since + # 6y = yD clears a denominator, the simplified relation multiplies h by 6 + # too: 6Y = 2*(-35xz^2) + 6*(x^3 + xz^2) = 6x^3 - 64xz^2. + fh = [[-56, 0, -75, 0, 15, 0, -1], [0, 1, 0, 1]] + assert simplify_hyperelliptic_scale(fh) == 1 + R = PolynomialRing(QQ, ['x', 'z']) + x, z = R.gens() + yD = comp_poly(fh, 6)(x, -35*x*z**2, z) + assert yD == 6*x**3 - 64*x*z**2 + assert yD != x**3 - 69*x*z**2 # only one copy of h, the old behavior + # the generator is 2-torsion, so yD vanishes on 3x^2 - 32z^2 = 0 + assert yD == 2*x*(3*x**2 - 32*z**2) + L = self.tc.get("/Genus2Curve/Q/336/a/172032/1") + page = L.get_data(as_text=True) + assert "-35xz^2" in page # minimal model, unchanged + assert "6x^3 - 64xz^2" in page + assert "x^3 - 69xz^2" not in page + + def test_model_code_snippets(self): + url = "/Genus2Curve/Q/169/a/169/1" + page = self.tc.get(url).get_data(as_text=True) + assert "Cmin := HyperellipticCurve(R![0, 0, 0, 0, 1, 1], R![1, 1, 0, 1])" in page + assert "Csim, pi := SimplifiedModel(Cmin);" in page + # every point is built on the model it belongs to, with no stale bare C + assert "Cmin![" in page and "Csim![" in page + assert not re.search(r"(? (2y+h)/s, where s = sqrt(n/squarefree_part(n)) xR = PolynomialRing(QQ, 'x') f = 4*xR(fh[0]) + xR(fh[1])**2 - f1 = xR(fh[1]) - n = gcd(f.coefficients()) + n = ZZ(gcd(f.coefficients())) + return (n // integer_squarefree_part(n)).isqrt() + + +def simplify_hyperelliptic_point(fh, pt): + s = simplify_hyperelliptic_scale(fh) + f1 = PolynomialRing(QQ, 'x')(fh[1]) xzR = PolynomialRing(QQ,['x', 'z']) z = xzR('z') f1 = (xzR(f1)*z**(4-len(fh[1]))).homogenize(z) - return [pt[0], (2*pt[1] + f1([pt[0],pt[2]])) / n, pt[2]] + return [pt[0], (2*pt[1] + f1([pt[0],pt[2]])) / s, pt[2]] -def comp_poly(fh): - xR = PolynomialRing(QQ, 'x') - f = 4*xR(fh[0]) + xR(fh[1])**2 - f1 = xR(fh[1]) - n = gcd(f.coefficients()) +def comp_poly(fh, d): + # Mordell-Weil generators are recorded with denominators cleared, as a relation + # d*y = yD(x,z) on the minimal model. Since y -> (2*y + h(x,z))/s, that same + # relation reads d*Y = (2*yD + d*h(x,z))/s on the simplified model, so the + # polynomial to substitute for yD depends on the denominator d. + s = simplify_hyperelliptic_scale(fh) + f1 = PolynomialRing(QQ, 'x')(fh[1]) xyzR = PolynomialRing(QQ,['x', 'y', 'z']) y = xyzR('y') z = xyzR('z') f1 = (xyzR(f1)*z**(4-len(fh[1]))).homogenize(z) - return (2*y + f1) / n + return (2*y + d*f1) / s def min_eqns_pretty(fh): @@ -609,7 +619,9 @@ def point_string(P): return '(' + ' : '.join(map(str, P)) + ')' -def mw_gens_table(invs,gens,hts,pts,comp=PolynomialRing(QQ,['x', 'y', 'z'])('y')): +# comp takes the cleared denominator of a generator's y-coordinate and returns the +# polynomial in x, y, z to substitute for that y-coordinate; None leaves it alone. +def mw_gens_table(invs,gens,hts,pts,comp=None): def divisor_data(P): R = PolynomialRing(QQ, ['x', 'z']) x = R('x') @@ -620,7 +632,8 @@ def divisor_data(P): if str(xD.factor())[:4] == "(-1)": xD = -xD yD = sum([ZZ(yden)*ZZ(yP[i][0])/ZZ(yP[i][1])*x**i*z**(len(yP)-i-1) for i in range(len(yP))]) - yD = comp(x, yD, z) + if comp is not None: + yD = comp(yden)(x, yD, z) return [make_bigint(elt, 10) for elt in [str(xD.factor()).replace("**","^").replace("*",""), str(yden)+"y" if yden > 1 else "y", str(yD).replace("**","^").replace("*","")]], xD, yD, yden if not invs: return '' @@ -647,7 +660,7 @@ def divisor_data(P): def mw_gens_simple_table(invs, gens, hts, pts, fh): spts = [simplify_hyperelliptic_point(fh, pt) for pt in pts] - return mw_gens_table(invs, gens, hts, spts, comp=comp_poly(fh)) + return mw_gens_table(invs, gens, hts, spts, comp=lambda d: comp_poly(fh, d)) def local_table(N, D, tama, bad_lpolys, bad_lfactors, cluster_pics, root_numbers): @@ -768,6 +781,21 @@ def augment_galrep_and_nonsurj(galrep, nonsurj): output.append({'prime': p, 'modell_image' : 'not computed'}) return output +def magma_cond_option(data): + """Options for Magma's Conductor(LSeries(...)), as a string to be inserted + just before the closing parenthesis of LSeries. + + Magma refuses to apply Ogg's formula once v_2(disc) >= 12, so for those + curves we hand it the local L-factor at 2 (which is in the database) rather + than letting it try. Both the code snippets on the curve page and the + downloaded code need this, so it lives here. + """ + if data['abs_disc'] % 4096 == 0: + ind2 = [a[0] for a in data['bad_lfactors']].index(2) + bad2 = data['bad_lfactors'][ind2][1] + return ': ExcFactors:=[*<2,Valuation(%s,2),R!%s>*]' % (data['cond'], bad2) + return '' + ############################################################################### # Genus 2 curve class definition @@ -1120,34 +1148,29 @@ def make_object(self, curve, endo, tama, ratpts, clus, galrep, nonsurj, is_curve code['show'] = {'sage':'', 'magma':''} # use default show names f,h = fh = data['min_eqn'] g = simplify_hyperelliptic(fh) - code['curve'] = {'sage':'R. = PolynomialRing(QQ); C = HyperellipticCurve(R(%s), R(%s));' % (f, h), - 'magma':'R := PolynomialRing(Rationals()); C := HyperellipticCurve(R!%s, R!%s);' % (f, h) } - code['simple_curve'] = {'sage':'X = HyperellipticCurve(R(%s))' % (g), 'magma':'X,pi:= SimplifiedModel(C);' } - if data['abs_disc'] % 4096 == 0: - ind2 = [a[0] for a in data['bad_lfactors']].index(2) - bad2 = data['bad_lfactors'][ind2][1] - magma_cond_option = ': ExcFactors:=[*<2,Valuation('+str(data['cond'])+',2),R!'+str(bad2)+'>*]' - else: - magma_cond_option = '' - code['cond'] = {'magma': 'Conductor(LSeries(C%s)); Factorization($1);' % magma_cond_option} - code['disc'] = {'magma':'Discriminant(C); Factorization(Integers()!$1);'} - code['geom_inv'] = {'sage':'C.igusa_clebsch_invariants(); [factor(a) for a in _]', - 'magma':'IgusaClebschInvariants(C); IgusaInvariants(C); G2Invariants(C);'} - code['aut'] = {'magma':'AutomorphismGroup(C); IdentifyGroup($1);'} - code['autQbar'] = {'magma':'AutomorphismGroup(ChangeRing(C,AlgebraicClosure(Rationals()))); IdentifyGroup($1);'} - code['num_rat_wpts'] = {'magma':'#Roots(HyperellipticPolynomials(SimplifiedModel(C)));'} + code['curve'] = {'sage':'R. = PolynomialRing(QQ); Cmin = HyperellipticCurve(R(%s), R(%s)) # minimal equation' % (f, h), + 'magma':'R := PolynomialRing(Rationals()); Cmin := HyperellipticCurve(R!%s, R!%s); // minimal equation' % (f, h) } + code['simple_curve'] = {'sage':'Csim = HyperellipticCurve(R(%s)); Csim # simplified equation' % (g), + 'magma':'Csim, pi := SimplifiedModel(Cmin); Csim; // simplified equation' } + code['cond'] = {'magma': 'Conductor(LSeries(Cmin%s)); Factorization($1);' % magma_cond_option(data)} + code['disc'] = {'magma':'Discriminant(Cmin); Factorization(Integers()!$1);'} + code['geom_inv'] = {'sage':'Cmin.igusa_clebsch_invariants(); [factor(a) for a in _]', + 'magma':'IgusaClebschInvariants(Cmin); IgusaInvariants(Cmin); G2Invariants(Cmin);'} + code['aut'] = {'magma':'AutomorphismGroup(Cmin); IdentifyGroup($1);'} + code['autQbar'] = {'magma':'AutomorphismGroup(ChangeRing(Cmin,AlgebraicClosure(Rationals()))); IdentifyGroup($1);'} + code['num_rat_wpts'] = {'magma':'#Roots(HyperellipticPolynomials(SimplifiedModel(Cmin)));'} if ratpts: - code['rat_pts'] = {'magma': '[' + ','.join("C![%s,%s,%s]" % (p[0], p[1], p[2]) for p in ratpts['rat_pts']) + ']; // minimal model'} - code['rat_pts_simp'] = {'magma': '[' + ','.join(["C![%s,%s,%s]" % (p[0], p[1], p[2]) for p in [simplify_hyperelliptic_point(data['min_eqn'], pt) for pt in ratpts['rat_pts']]]) + ']; // simplified model'} - code['mw_group'] = {'magma':'MordellWeilGroupGenus2(Jacobian(C));'} - code['two_selmer'] = {'magma':'TwoSelmerGroup(Jacobian(C)); NumberOfGenerators($1);'} - code['has_square_sha'] = {'magma':'HasSquareSha(Jacobian(C));'} - code['locally_solvable'] = {'magma':'f,h:=HyperellipticPolynomials(C); g:=4*f+h^2; HasPointsEverywhereLocally(g,2) and (#Roots(ChangeRing(g,RealField())) gt 0 or LeadingCoefficient(g) gt 0);'} - code['torsion_subgroup'] = {'magma':'TorsionSubgroup(Jacobian(SimplifiedModel(C))); AbelianInvariants($1);'} - code['decomp'] = {'magma':'HeuristicDecompositionFactors(C);'} + code['rat_pts'] = {'magma': '[' + ','.join("Cmin![%s,%s,%s]" % (p[0], p[1], p[2]) for p in ratpts['rat_pts']) + ']; // minimal model'} + code['rat_pts_simp'] = {'magma': '[' + ','.join(["Csim![%s,%s,%s]" % (p[0], p[1], p[2]) for p in [simplify_hyperelliptic_point(data['min_eqn'], pt) for pt in ratpts['rat_pts']]]) + ']; // simplified model'} + code['mw_group'] = {'magma':'MordellWeilGroupGenus2(Jacobian(Cmin));'} + code['two_selmer'] = {'magma':'TwoSelmerGroup(Jacobian(Cmin)); NumberOfGenerators($1);'} + code['has_square_sha'] = {'magma':'HasSquareSha(Jacobian(Cmin));'} + code['locally_solvable'] = {'magma':'f,h:=HyperellipticPolynomials(Cmin); g:=4*f+h^2; HasPointsEverywhereLocally(g,2) and (#Roots(ChangeRing(g,RealField())) gt 0 or LeadingCoefficient(g) gt 0);'} + code['torsion_subgroup'] = {'magma':'TorsionSubgroup(Jacobian(SimplifiedModel(Cmin))); AbelianInvariants($1);'} + code['decomp'] = {'magma':'HeuristicDecompositionFactors(Cmin);'} code['endos0'] = {'magma':'//Please install CHIMP (https://github.com/edgarcosta/CHIMP) if you want to run this code'} - code['endos1'] = {'magma':'HeuristicIsGL2(C); HeuristicEndomorphismDescription(C); HeuristicEndomorphismFieldOfDefinition(C);'} - code['endos2'] = {'magma':'HeuristicIsGL2(C : Geometric := true); HeuristicEndomorphismDescription(C : Geometric := true); HeuristicEndomorphismLatticeDescription(C);'} + code['endos1'] = {'magma':'HeuristicIsGL2(Cmin); HeuristicEndomorphismDescription(Cmin); HeuristicEndomorphismFieldOfDefinition(Cmin);'} + code['endos2'] = {'magma':'HeuristicIsGL2(Cmin : Geometric := true); HeuristicEndomorphismDescription(Cmin : Geometric := true); HeuristicEndomorphismLatticeDescription(Cmin);'} self._code = None @@ -1161,5 +1184,6 @@ def get_code(self): # Fill in placeholders for this specific curve: for lang in ['magma']: #TODO: 'sage', 'pari', self._code['curve'][lang] = self._code['curve'][lang] % (self.data['min_eqn']) + self._code['cond'][lang] = self._code['cond'][lang] % magma_cond_option(self.data) return self._code