generate.py

back to table · edit · history · where entries came from · files · download

7249 bytes, as of the version from 2026-11-01 11:13. Recorded here, not run.

"""Mahler measures of the Hessian family -- numberdb.org/T649.

Run with SageMath and the numberdb client installed:
    sage -pip install numberdb
    sage -python generate.py                 # verify every stored entry
    sage -python generate.py --fill          # fill the existing draft

The numerical method is Jensen's formula for y^2+(x^2-k*x)*y+x,
where k is the real cube root of t. The roots have product x, of modulus
one; the logarithm of the larger modulus equals |Re acosh(z)| with
z=(k*exp(i*theta/2)-exp(3*i*theta/2))/2. Tanh-sinh quadrature is split
at every torus crossing. Two precisions, digits+40 and digits+90, must
agree with a margin of 15 digits. This is an agreement check, not a
certified integration bound. No floating result is disguised as a ball.

The only parameter is t. The exact catalogue and selection rationale are
in argument-selection.md. Selection-relevant source dependency: this file
alone. checks.py supplies independent cubic Jensen and hypergeometric
checks and exact CM checks. The source catalogue is Samart--Tao v3,
arXiv:2409.14714, Table 1, rows 13--27.
"""
import os
import sys
import json
from functools import lru_cache
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.qqbar import AA
import mpmath as mp

# row, rational part, radical coefficient, radicand, CM discriminant.
CATALOGUE = (
    (13, '108', '54', 2, -24),
    (14, '18', '6', 3, -36),
    (15, '270', '162', 3, -4),
    (16, '729', '405', 3, -48),
    (17, '-27/2', '-27/2', 5, -15),
    (18, '3375/2', '1485/2', 5, -60),
    (19, '-4320', '-1944', 5, -75),
    (20, '32', '2', 6, -72),
    (21, '3672', '1458', 6, -8),
    (22, '-864', '-216', 17, -51),
    (23, '-163296', '-35640', 21, -147),
    (24, '4', '-4', 33, -99),
    (25, '-16740', '-2916', 33, -11),
    (26, '-55296', '-8640', 41, -123),
    (27, '-13500000', '-1431000', 89, -267),
)


def catalogue():
    result = []
    for text in ('-1', '0', '27', '-216', '24', '54'):
        result.append(dict(key=text, a=text, b='0', d=1,
                           role='intrinsic' if text in ('-1','0','27') else 'arithmetic_structure',
                           D={'-216':-3,'24':-27,'54':-12}.get(text)))
    for row, a, b, d, D in CATALOGUE:
        for sign in (1, -1):
            coefficient = sign*QQ(b)
            key = a+('+' if coefficient>0 else '-')+str(abs(coefficient))+'*sqrt('+str(d)+')'
            result.append(dict(key=key, a=a, b=str(coefficient), d=d,
                               row=row, D=D, role='arithmetic_structure'))
    return result

ITEMS = catalogue()
BY_KEY = {item['key']: item for item in ITEMS}


def exact_argument(item):
    return AA(QQ(item['a'])) + AA(QQ(item['b']))*AA(item['d']).sqrt()


def mp_rational(s):
    parts=s.split('/')
    return mp.mpf(parts[0])/mp.mpf(parts[1]) if len(parts)==2 else mp.mpf(s)


def mp_argument(item):
    return mp_rational(item['a'])+mp_rational(item['b'])*mp.sqrt(item['d'])


def compute(item, working):
    # mpmath retains transformed nodes for every interval and precision.
    # Bound that cache across values; no numerical result is reused here.
    mp.mp._tanh_sinh.clear()
    with mp.workdps(working):
        t=mp_argument(item)
        k=mp.sign(t)*mp.root(abs(t),3)
        def integrand(theta):
            z=(k*mp.exp(mp.j*theta/2)-mp.exp(3*mp.j*theta/2))/2
            return abs(mp.re(mp.acosh(z)))
        points=[mp.mpf(0),mp.pi]
        if -1<t<27:
            points.insert(1,mp.acos((k-1)/2))
        # Midpoints make each interval's independent convergence less sensitive
        # to a distant complex singularity. They do not select table arguments.
        split=[points[0]]
        for a,b in zip(points,points[1:]):
            split.extend([(a+b)/2,b])
        tolerance=mp.power(10,-working+20)
        def integrate(a,b,depth=0):
            value,error=mp.quad(integrand,[a,b],error=True,maxdegree=8)
            if error <= tolerance*(b-a)/mp.pi:
                return value,error
            if depth>=24:
                raise ArithmeticError('Adaptive quadrature depth exhausted for '+item['key'])
            middle=(a+b)/2
            left,el=integrate(a,middle,depth+1)
            right,er=integrate(middle,b,depth+1)
            return left+right,el+er
        pieces=[integrate(a,b) for a,b in zip(split,split[1:])]
        value=sum(v for v,e in pieces)/mp.pi
        error=sum(e for v,e in pieces)/mp.pi
        if not mp.isfinite(value) or error>mp.power(10,-working+20):
            raise ArithmeticError('Quadrature failed convergence for '+item['key']+' at '+str(working)+' digits: estimate='+str(error))
        return mp.nstr(value,working),mp.nstr(error,8)


@lru_cache(maxsize=None)
def evaluated(key,digits):
    item=BY_KEY[key]
    low,err_low=compute(item,digits+40)
    high,err_high=compute(item,digits+90)
    with mp.workdps(digits+100):
        difference=abs(mp.mpf(low)-mp.mpf(high))
        scale=max(mp.mpf(1),abs(mp.mpf(high)))
        agreement=-mp.log10(difference/scale) if difference else mp.inf
        if agreement<digits+15:
            raise ArithmeticError('Insufficient precision agreement for '+key)
        return dict(number=mp.nstr(mp.mpf(high),digits,strip_zeros=False),
                    low=low, high=high, agreement=str(agreement),
                    quadrature_estimates=[err_low,err_high])


def radical_label(item):
    if item['b']=='0':
        return None
    def rational_latex(s):
        q=QQ(s)
        return str(q.numerator()) if q.denominator()==1 else r'\frac{'+str(q.numerator())+'}{'+str(q.denominator())+'}'
    a=rational_latex(item['a'])
    b=QQ(item['b'])
    return '$'+a+('+' if b>0 else '-')+rational_latex(str(abs(b)))+r'\sqrt{'+str(item['d'])+'}$'


class HessianMahler(numberdb.Generator):
    table='T649'
    parameters=('t',)
    type='R'
    digits=100
    rigour='heuristic (agreement-checked)'

    def environment(self):
        return dict(super().environment(), mpmath=mp.__version__)

    def enumerate(self):
        for item in ITEMS:
            yield {'t':item['key']}

    def value(self,params,digits):
        item=BY_KEY[str(params['t'])]
        out={'number':evaluated(item['key'],digits)['number']}
        label=radical_label(item)
        if label:
            out['param-latex']=label
        comments={
            '-1':r'The negative real boundary of the torus-zero region, with $k=-1$.',
            '0':r'The Fermat specialization gives $N(0)=\mu_3$, the three-step value in HREF{Mahler_measures_of_(short_random_walks)#3}.',
            '27':r'The cubic factors into three linear factors, each of Mahler measure $\mu_3$, so $N(27)=3\mu_3$.'}
        if item['key'] in comments:
            out['comment']=comments[item['key']]
        return out


if __name__=='__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        numberdb.configure(api_key=sys.stdin.read().strip())
    g=HessianMahler()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH')=='1':
        print(g.publish(message='Fill 36 ordinary Hessian Mahler measures with 100 agreement-checked digits'))
    else:
        report=g.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)