Skip to content

Commit e2039c3

Browse files
authored
Merge pull request #7162 from roed-math/ai/t39-padic-nicknames
Add nicknames for unramified and tame p-adic fields
2 parents 7b77062 + df0ccc4 commit e2039c3

3 files changed

Lines changed: 159 additions & 25 deletions

File tree

lmfdb/local_fields/family.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,9 @@ def gamma(self):
334334
@lazy_attribute
335335
def base_link(self):
336336
from .main import pretty_link
337-
return pretty_link(self.base, self.p, self.n0, self.rf0)
337+
cols = ["new_label", "old_label", "p", "n", "e", "f", "rf", "eisen", "unram"]
338+
data = db.lf_fields.lucky({"new_label": self.base}, cols)
339+
return pretty_link(self.base, self.p, self.n0, self.rf0, data)
338340

339341
def __iter__(self):
340342
# TODO: This needs to be fixed when base != Qp

lmfdb/local_fields/main.py

Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from flask import abort, render_template, request, url_for, redirect, make_response
88
from sage.all import (
9-
PolynomialRing, ZZ, QQ, RR, latex, cached_function, Integers, euler_phi, is_prime)
9+
PolynomialRing, ZZ, QQ, RR, GF, gcd, latex, cached_function, Integers, euler_phi, is_prime)
1010
from sage.plot.all import line, points, text, Graphics, polygon
1111

1212
from lmfdb import db
@@ -141,8 +141,9 @@ def local_field_data(label):
141141
else:
142142
return "Invalid label %s" % label
143143
nicename = ''
144-
if f['n'] < 3:
145-
nicename = ' = ' + prettyname(f)
144+
nick = field_nickname(f)
145+
if nick is not None:
146+
nicename = ' = ' + nick
146147
ans = '$p$-adic field %s%s<br><br>' % (label, nicename)
147148
ans += r'Extension of $\Q_{%s}$ defined by %s<br>' % (str(f['p']),web_latex(coeff_to_poly(f['coeffs'])))
148149
gn = f['n']
@@ -295,10 +296,11 @@ def ctx_local_fields():
295296

296297
# Utilities for subfield display
297298
def format_lfield(label, p):
299+
cols = ["n", "p", "e", "f", "rf", "eisen", "unram", "old_label", "new_label"]
298300
if OLD_LF_RE.fullmatch(label):
299-
data = db.lf_fields.lucky({"old_label": label}, ["n", "p", "rf", "old_label", "new_label"])
301+
data = db.lf_fields.lucky({"old_label": label}, cols)
300302
else:
301-
data = db.lf_fields.lucky({"new_label": label}, ["n", "p", "rf", "old_label", "new_label"])
303+
data = db.lf_fields.lucky({"new_label": label}, cols)
302304
return lf_display_knowl(label, name=prettyname(data))
303305

304306

@@ -591,13 +593,16 @@ def display(self, rec):
591593
return r"$100\%$"
592594
return fr"${100*x:.2f}\%$"
593595

594-
def pretty_link(label, p, n, rf):
595-
if OLD_LF_RE.fullmatch(label):
596-
name = {"old_label": label}
597-
else:
598-
name = {"new_label": label}
599-
name.update({"p": p, "n": n, "rf": rf})
600-
name = prettyname(name)
596+
def pretty_link(label, p, n, rf, data=None):
597+
if data is None:
598+
# Minimal record: enough for the Q_p(sqrt(d)) nickname of quadratics, and a
599+
# label fallback otherwise.
600+
if OLD_LF_RE.fullmatch(label):
601+
data = {"old_label": label}
602+
else:
603+
data = {"new_label": label}
604+
data.update({"p": p, "n": n, "rf": rf})
605+
name = prettyname(data)
601606
return f'<a href="{url_for_label(label)}">{name}</a>'
602607

603608
families_columns = SearchColumns([
@@ -616,9 +621,9 @@ def pretty_link(label, p, n, rf):
616621
MathCol("c0", "lf.discriminant_exponent", "$c_0$", short_title="base disc. exponent", default=False, contingent=lambda info: "relative" in info),
617622
MathCol("c_absolute", "lf.discriminant_exponent", r"$c_{\mathrm{abs}}$", short_title="abs. disc. exponent", default=False, contingent=lambda info: "relative" in info),
618623
MultiProcessedCol("base_field", "lf.family_base", "Base",
619-
["base", "p", "n0", "rf0"],
624+
["base", "p", "n0", "rf0", "base_data"],
620625
pretty_link,
621-
apply_download=lambda base, p, n0, rf0: base,
626+
apply_download=lambda base, p, n0, rf0, base_data: base,
622627
contingent=lambda info: "relative" in info),
623628
RationalListCol("visible", "lf.slopes", "Abs. Artin slopes",
624629
show_slopes2, default=False, short_title="abs. Artin slopes"),
@@ -648,14 +653,20 @@ def lf_postprocess(res, info, query):
648653
return res
649654

650655
def families_postprocess(res, info, query):
651-
quads = list(set(rec["base"] for rec in res if rec["n0"] == 2))
652-
if quads:
653-
rflook = {rec["new_label"]: rec["rf"] for rec in db.lf_fields.search({"new_label":{"$in":quads}}, ["new_label", "rf"])}
656+
# Fetch the base field of each family so its "Base" column shows a nickname.
657+
bases = list({rec["base"] for rec in res})
658+
base_lookup = {}
659+
if bases:
660+
cols = ["new_label", "old_label", "p", "n", "e", "f", "rf", "eisen", "unram"]
661+
base_lookup = {rec["new_label"]: rec
662+
for rec in db.lf_fields.search({"new_label": {"$in": bases}}, cols)}
654663
for rec in res:
664+
bdata = base_lookup.get(rec["base"])
665+
rec["base_data"] = bdata
655666
if rec["n0"] == 1:
656667
rec["rf0"] = [1, 0]
657-
elif rec["n0"] == 2:
658-
rec["rf0"] = rflook[rec["base"]]
668+
elif bdata is not None:
669+
rec["rf0"] = bdata.get("rf")
659670
else:
660671
rec["rf0"] = None
661672
return res
@@ -1195,12 +1206,117 @@ def render_field_webpage(args):
11951206
KNOWL_ID="lf.%s" % label, # TODO: BROKEN
11961207
)
11971208

1209+
def _field_label(ent):
1210+
return ent.get('new_label') or ent.get('old_label')
1211+
1212+
@cached_function
1213+
def _lf_residue_field(p, f, unram):
1214+
# Fq together with tbar, the image in Fq of a root of the (Conway) polynomial
1215+
# `unram`. tbar is a multiplicative generator of Fq^* (Conway polynomials are
1216+
# primitive), so its Teichmuller lift is our chosen zeta_{p^f-1}.
1217+
modpoly = PolynomialRing(GF(p), 't')(unram)
1218+
if f == 1:
1219+
Fp = GF(p)
1220+
return Fp, -Fp(modpoly[0]) # root of the monic linear t + c is -c
1221+
Fq = GF(p**f, 'tt', modulus=modpoly)
1222+
return Fq, Fq.gen()
1223+
1224+
def _frob_min(k, p, g):
1225+
# Smallest element of the Frobenius orbit {k*p^j mod g}: k and p*k index
1226+
# Q_p-isomorphic tame fields (the Frobenius twist zeta -> zeta^p of U/Q_p).
1227+
orbit = set()
1228+
x = k % g
1229+
while x not in orbit:
1230+
orbit.add(x)
1231+
x = (x * p) % g
1232+
return min(orbit)
1233+
1234+
def _tame_nickname(p, e, f, eisen, unram):
1235+
# Nickname for a tamely and genuinely ramified field (e > 1, p does not divide e).
1236+
# Such a field is U(pi) with pi^e = zeta^k*p up to an e-th power, U = Q_p(zeta_m),
1237+
# zeta = zeta_m the Teichmuller lift of tbar, m = p^f-1. From the Eisenstein
1238+
# polynomial, pi^e = -a0*(1+M) with M in the maximal ideal, and 1+M is an e-th
1239+
# power (tame), so zeta^k*p ~ -a0 = p*w gives zeta^k ~ w mod (K^*)^e, i.e.
1240+
# tbar^k = wbar mod (Fq^*)^g with g = gcd(e, m).
1241+
try:
1242+
Fq, tbar = _lf_residue_field(p, f, unram)
1243+
m = p**f - 1
1244+
g = gcd(e, m)
1245+
Ptx = PolynomialRing(PolynomialRing(ZZ, 't'), 'x')
1246+
a0 = Ptx(str(eisen))[0] # constant term, an element of Z[t] = Z_q
1247+
wbar = Fq(0)
1248+
for i, c in enumerate(a0.list()):
1249+
c = ZZ(c)
1250+
if c % p != 0: # a0 should have p-adic valuation 1 (Eisenstein)
1251+
return None
1252+
wbar += Fq(-(c // p)) * tbar**i
1253+
if wbar == 0:
1254+
return None
1255+
if g == 1:
1256+
k = 0
1257+
else:
1258+
exp = m // g
1259+
target = wbar**exp
1260+
base = tbar**exp
1261+
cur = Fq(1)
1262+
k = None
1263+
for kk in range(g):
1264+
if cur == target:
1265+
k = kk
1266+
break
1267+
cur *= base
1268+
if k is None:
1269+
return None
1270+
k = _frob_min(k, p, g)
1271+
except (TypeError, ValueError, ZeroDivisionError):
1272+
return None
1273+
Qp = r'\Q_{%s}' % p
1274+
root = r'\sqrt' if e == 2 else r'\sqrt[%s]' % e
1275+
if f == 1:
1276+
# zeta_{p-1} lies in Q_p; zeta^k is the Teichmuller lift of tbar^k, and any
1277+
# integer r = tbar^k mod p differs from it by an e-th power, so use radicand p*r.
1278+
r = ZZ(tbar**k)
1279+
if r == 1:
1280+
rad = str(p)
1281+
elif r == p - 1:
1282+
rad = '-' + str(p)
1283+
else:
1284+
rad = r'%s \cdot %s' % (p, r)
1285+
return r'$%s(%s{%s})$' % (Qp, root, rad)
1286+
if k == 0:
1287+
rad = str(p)
1288+
elif 2 * k == m:
1289+
rad = '-' + str(p) # zeta_m^{m/2} = -1
1290+
else:
1291+
zpow = r'\zeta_{%s}' % m if k == 1 else r'\zeta_{%s}^{%s}' % (m, k)
1292+
rad = r'%s \cdot %s' % (zpow, p)
1293+
return r'$%s(\zeta_{%s}, %s{%s})$' % (Qp, m, root, rad)
1294+
1295+
def field_nickname(ent):
1296+
# A human-readable name for a p-adic field, or None if we have no nice one.
1297+
# Quadratics keep the existing Q_p(sqrt(d)) form; unramified extensions become
1298+
# Q_p(zeta_{p^f-1}); tame extensions Q_p(zeta_{p^f-1}, root(zeta^k p)); wildly
1299+
# ramified fields have no nickname (fall back to the label).
1300+
p, n = ent.get('p'), ent.get('n')
1301+
if p is None or n is None:
1302+
return None
1303+
if n <= 2:
1304+
rf = ent.get('rf')
1305+
return printquad(rf, p) if rf is not None else None
1306+
e, f = ent.get('e'), ent.get('f')
1307+
if e is None or f is None:
1308+
return None
1309+
if e % p == 0:
1310+
return None # wildly ramified: no nice nickname
1311+
if e == 1:
1312+
return r'$\Q_{%s}(\zeta_{%s})$' % (p, p**f - 1) # unramified
1313+
eisen, unram = ent.get('eisen'), ent.get('unram')
1314+
if eisen is None or unram is None:
1315+
return None
1316+
return _tame_nickname(p, e, f, eisen, unram)
1317+
11981318
def prettyname(ent):
1199-
if ent['n'] <= 2:
1200-
return printquad(ent['rf'], ent['p'])
1201-
if ent.get('new_label'):
1202-
return ent['new_label']
1203-
return ent['old_label']
1319+
return field_nickname(ent) or _field_label(ent)
12041320

12051321
@cached_function
12061322
def getu(p):

lmfdb/local_fields/test_localfields.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ def test_field_page(self):
2929
assert 'x^{2} + 7 x + 2' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find...
3030
assert 'x^{3} + 44 t + 99' in L.get_data(as_text=True) # bad (not robust) test, but it's the best i was able to find...
3131

32+
def test_nicknames(self):
33+
# Unramified extensions are nicknamed Q_p(zeta_{p^f-1}). Issue #6201.
34+
page = self.tc.get('/padicField/2.3.1.0a1.1', follow_redirects=True).get_data(as_text=True)
35+
assert r'\Q_{2}(\zeta_{7})' in page
36+
# Tamely ramified with f = 1: Q_p(root(p*r)) (matches the quadratic sqrt(p*u) form).
37+
page = self.tc.get('/padicField/7.1.3.2a1.2', follow_redirects=True).get_data(as_text=True)
38+
assert r'\sqrt[3]{7 \cdot 2}' in page
39+
# Tamely ramified with f > 1: Q_p(zeta_{p^f-1}, root(zeta^k p)).
40+
page = self.tc.get('/padicField/5.2.3.4a1.1', follow_redirects=True).get_data(as_text=True)
41+
assert r'\zeta_{24}' in page and r'\sqrt[3]{\zeta_{24} \cdot 5}' in page
42+
page = self.tc.get('/padicField/2.2.3.4a1.2', follow_redirects=True).get_data(as_text=True)
43+
assert r'\Q_{2}(\zeta_{3}, \sqrt[3]{2})' in page
44+
# Wildly ramified fields have no nickname: the title falls back to the label.
45+
page = self.tc.get('/padicField/2.1.4.6a1.1', follow_redirects=True).get_data(as_text=True)
46+
assert r'$p$-adic field 2.1.4.6a1.1' in page
47+
3248
def test_global_splitting_models(self):
3349
# The first one will have to change if we compute a GSM for it
3450
L = self.tc.get('/padicField/163.1.8.7a1.2')

0 commit comments

Comments
 (0)