back to table · edit · history · where entries came from · files · download
9556 bytes, as of the version from 2026-09-21 17:42 (current). Recorded here, not run.
"""Zeros of the Dedekind zeta functions of cubic fields -- numberdb.org/T385.
For each cubic field K with |D_K| <= 500, this stores the first ten positive
ordinates t_n for zeros zeta_K(1/2 + i*t_n) = 0 on the critical line.
Run it with SageMath:
$ sage -pip install numberdb # once
$ sage -python generate.py # check the table against this code
$ sage -python generate.py --publish # fill the draft, with NUMBERDB_API_KEY set
Under this repository's build wrapper:
$ cat "$NUMBERDB_KEY_FILE" | NUMBERDB_KEY_FROM_STDIN=1 NUMBERDB_PUBLISH=1 \
agents/sage.sh generators/dedekind-zeta-zeros-cubic-fields/generate.py
"""
from decimal import Decimal
import os
import sys
import numberdb.sage as numberdb
from sage.libs.pari import pari
BOUND = 500
EXPECTED_FIELDS = 70
EXPECTED_NEGATIVE = 58
EXPECTED_POSITIVE = 12
ZEROS_PER_FIELD = 10
SEARCH_HEIGHT = 60
ZERO_MESH = 16
DIGITS = 30
WORKING_DIGITS = (80, 120)
CHECK_DIGITS = 80
GROUPS = ("C3", "S3")
T4_MATCHES = {
(49, 1): {
1: "HREF{Zeros_of_Dirichlet_L_functions#7,2,1}[T4 $(7,2,1)$]",
2: "HREF{Zeros_of_Dirichlet_L_functions#7,4,1}[T4 $(7,4,1)$]",
3: "HREF{Zeros_of_Dirichlet_L_functions#7,4,2}[T4 $(7,4,2)$]",
4: "HREF{Zeros_of_Dirichlet_L_functions#7,2,2}[T4 $(7,2,2)$]",
5: "HREF{Zeros_of_Dirichlet_L_functions#7,2,3}[T4 $(7,2,3)$]",
6: "HREF{Zeros_of_Dirichlet_L_functions#7,4,3}[T4 $(7,4,3)$]",
},
}
_FIELDS = None
_BY_IDENTITY = None
_DATA = {}
def _key_from_stdin():
if os.environ.get("NUMBERDB_KEY_FROM_STDIN") != "1":
return
token = sys.stdin.read().strip()
if "=" in token and token.split("=", 1)[0].isupper():
token = token.split("=", 1)[1].strip().strip("'\"")
if token:
os.environ["NUMBERDB_API_KEY"] = token
numberdb.configure(api_key=token)
def _pari_poly(poly):
return str(poly)
def _pari_decimal(value):
return Decimal(str(value).replace(" ", ""))
def _field_discriminant(poly):
return int(pari("nfdisc(%s)" % _pari_poly(poly)))
def _coeff_key(poly):
degree = int(pari("poldegree(%s)" % _pari_poly(poly)))
return tuple(
int(pari("polcoef(%s,%d)" % (_pari_poly(poly), exponent)))
for exponent in range(degree - 1, -1, -1)
)
def _field_label(field):
r1 = 3 if field["D"] > 0 else 1
return "3.%d.%d.%d" % (r1, abs(field["D"]), field["k"])
def _fields(bound=BOUND):
global _FIELDS, _BY_IDENTITY
if _FIELDS is not None:
return _FIELDS
found = {}
for group in GROUPS:
for raw in pari('nflist("%s", [1, %d])' % (group, bound)):
reduced = pari("polredabs(%s)" % _pari_poly(raw))
discriminant = _field_discriminant(reduced)
if abs(discriminant) > bound:
continue
key = str(reduced)
if key in found and found[key]["group"] != group:
raise ArithmeticError("%s was returned for two Galois groups" % reduced)
found[key] = {
"poly": reduced,
"D": discriminant,
"group": group,
}
grouped = {}
for field in found.values():
grouped.setdefault(field["D"], []).append(field)
rows = []
for discriminant in sorted(grouped, key=lambda d: (abs(d), d < 0)):
fields = sorted(grouped[discriminant], key=lambda field: _coeff_key(field["poly"]))
for index, field in enumerate(fields, 1):
row = dict(field)
row["k"] = index
row["lmfdb_label"] = _field_label(row)
rows.append(row)
negative = sum(1 for row in rows if row["D"] < 0)
positive = sum(1 for row in rows if row["D"] > 0)
if (len(rows), negative, positive) != (
EXPECTED_FIELDS,
EXPECTED_NEGATIVE,
EXPECTED_POSITIVE,
):
raise ArithmeticError(
"expected %d fields, %d negative and %d positive; found %d, %d, %d"
% (
EXPECTED_FIELDS,
EXPECTED_NEGATIVE,
EXPECTED_POSITIVE,
len(rows),
negative,
positive,
)
)
_FIELDS = tuple(rows)
_BY_IDENTITY = {(row["D"], row["k"]): row for row in rows}
return _FIELDS
def _field_by_identity(D, k):
if _BY_IDENTITY is None:
_fields()
field = _BY_IDENTITY.get((int(D), int(k)))
if field is None:
raise KeyError("no cubic field at D=%s, k=%s" % (D, k))
return field
def _zeros_for_field(field, working_digits):
pari("default(realprecision,%d)" % int(working_digits))
ldata = pari("lfuncreate(bnfinit(%s,1))" % _pari_poly(field["poly"]))
zeros = pari("lfunzeros")(ldata, SEARCH_HEIGHT, ZERO_MESH)
positive = [zero for zero in zeros if _pari_decimal(zero) > 0]
if len(positive) < ZEROS_PER_FIELD:
raise ArithmeticError(
"D=%s k=%s has only %d positive zeros below height %d"
% (field["D"], field["k"], len(positive), SEARCH_HEIGHT)
)
return tuple(str(zero) for zero in positive[:ZEROS_PER_FIELD])
def _data_for_precision(working_digits):
working_digits = int(working_digits)
if working_digits in _DATA:
return _DATA[working_digits]
rows = {}
for field in _fields():
rows[(field["D"], field["k"])] = _zeros_for_field(field, working_digits)
_DATA[working_digits] = rows
return rows
def _zero_text(D, k, n, working_digits):
return _data_for_precision(working_digits)[(int(D), int(k))][int(n) - 1]
def _field_comment(field, n):
signature = "$(3,0)$" if field["D"] > 0 else "$(1,1)$"
polynomial = str(field["poly"]).replace("*", "").replace(" ", "")
text = (
"$%s=0$; signature %s; %s; LMFDB %s."
% (polynomial, signature, field["group"], field["lmfdb_label"])
)
match = T4_MATCHES.get((field["D"], field["k"]), {}).get(int(n))
if match:
text += " Also %s." % match
return text
def _assert_cyclic_controls():
"""The conductor-7 cyclic cubic field factors through T4's two characters."""
expected = {
1: "4.35640162473628422727957479051",
2: "6.20123004275588129466099054628",
3: "7.92743089809203774838798659746",
4: "8.78555471449907536558015746317",
5: "10.73611998749339311587424153504",
6: "11.01044486207249042239362741094",
}
for n, text in expected.items():
got = _zero_text(49, 1, n, CHECK_DIGITS)
if not got.startswith(text):
raise ArithmeticError("D=49 n=%d gave %s, expected prefix %s" % (n, got, text))
print("matched the first six D=49 cyclic zeros with T4 prefixes")
class CubicDedekindZetaZeros(numberdb.Generator):
table = os.environ.get("NUMBERDB_TABLE", "T385")
parameters = ("D", "k", "n")
type = "R"
digits = DIGITS
rigour = "heuristic (agreement-checked)"
files = ("generate.py",)
def enumerate(self, bound=BOUND):
if bound != BOUND:
raise ValueError("this generator is written for bound %d" % BOUND)
for field in _fields():
for n in range(1, ZEROS_PER_FIELD + 1):
yield {"D": field["D"], "k": field["k"], "n": n}
def value(self, params, digits):
D = int(params["D"])
k = int(params["k"])
n = int(params["n"])
field = _field_by_identity(D, k)
return {
"number": numberdb.agreeing(
lambda working: _zero_text(D, k, n, working),
at=WORKING_DIGITS,
),
"comment": _field_comment(field, n),
}
def fill_draft_once(generator, message):
"""Fill a fresh prose draft without the client's empty upsert probe."""
from numberdb._generate import (
_check_precision,
_check_rigour,
_producer,
_run_name,
_source_files,
)
from numberdb._write import Entries, attach, submit_entries, to_text
table = generator.table
run = _run_name(generator)
entries = Entries(*generator.parameters)
for params in generator.enumerate():
params = dict(params)
wanted = generator.digits_for(params)
entry = generator._entry(params, wanted)
value = entry["number"]
identity = ",".join(str(params[name]) for name in generator.parameters)
_check_rigour(generator, table, identity, value)
written = to_text(value, wanted, generator.format)
_check_precision(table, identity, written, wanted, lowering=False)
record = dict(entry)
record.pop("digits", None)
entries.add(**params, **record, digits=wanted)
answer = submit_entries(
table,
entries,
message=message,
produced_by=_producer(generator, os.environ.get("NUMBERDB_ASSISTED_BY", "")),
upsert=False,
run=run,
rigour=generator.rigour,
)
for name, body in sorted(_source_files(generator).items()):
attach(table, name, body, run=run, message=message, rigour=generator.rigour)
return answer
if __name__ == "__main__":
_key_from_stdin()
generator = CubicDedekindZetaZeros()
_assert_cyclic_controls()
if os.environ.get("NUMBERDB_PUBLISH") == "1" or "--publish" in sys.argv:
print(fill_draft_once(
generator,
message="computed cubic Dedekind zeta zero ordinates",
))
else:
report = generator.verify(sample=None)
print(report)
sys.exit(0 if report.ok else 1)