generate.py

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

7562 bytes, as of the version from 2026-10-25 05:01 (current). Recorded here, not run.

"""Critical exponents of the three-dimensional universality classes -- numberdb.org/T472.

Run with SageMath (or passagemath with numberdb installed):
    sage -pip install numberdb
    sage -python generate.py                  # verify every stored entry
    sage -python checks.py                    # offline numerical checks
    sage -python generate.py --fill-draft     # write entries to this draft

The authenticated operations need NUMBERDB_API_KEY. The campaign can supply
it on stdin with NUMBERDB_KEY_FROM_STDIN=1. --fill-draft writes numbers only;
it neither publishes the table nor offers it for review.

Published estimates, not fresh bootstrap or Monte Carlo computations, are
transcribed here. Increasing digits cannot increase their precision. Direct
inputs retain the source's uncertainty. Derived values enclose the image of
the rectangular input error box using exact rational arithmetic; the radius
is rounded upward to two significant figures. This does not turn empirical
error bars into rigorous bounds or statistical confidence intervals.
"""
import numberdb.sage as numberdb
from fractions import Fraction as Q
from itertools import product
import os
import sys

CLASSES = ('ising', 'xy', 'heisenberg', 'saw', 'percolation')
EXPONENTS = ('alpha', 'beta', 'gamma', 'delta', 'nu', 'eta', 'omega')
# Strings are centre +/- radius, with no binary floating-point conversion.
INPUTS = {
    'ising': {'nu': '0.62997097 +/- 0.00000012',
              'eta': '0.036297612 +/- 0.000000048',
              'omega': '0.82951 +/- 0.00061'},
    'xy': {'nu': '0.67169 +/- 0.00007', 'eta': '0.03810 +/- 0.00008',
           'omega': '0.789 +/- 0.004'},
    'heisenberg': {'nu': '0.71164 +/- 0.00010', 'eta': '0.03784 +/- 0.00005',
                   'omega': '0.759 +/- 0.002'},
    'saw': {'nu': '0.58759700 +/- 0.00000040',
            'gamma': '1.15695300 +/- 0.00000095', 'Delta1': '0.528 +/- 0.008'},
    # Table 5 gives separate statistical and omega-fit errors. We add them
    # conservatively: .0016+.0002 and .00027+.00007, without assuming independence.
    'percolation': {'nu': '0.8765 +/- 0.0018', 'eta': '-0.04602 +/- 0.00034',
                    'omega': '1.62 +/- 0.13'},
}
SOURCES = {'ising': 'Chang2025', 'xy': 'Hasenbusch2019',
           'heisenberg': 'Hasenbusch2020', 'percolation': 'Ballesteros1999'}


def parse(text):
    c, r = (Q(s.strip()) for s in text.split('+/-'))
    assert r > 0
    return c-r, c+r


def exponent_at(nu, second, key, walk=False):
    # Algebraically direct in (nu,gamma) for SAW, retaining their dependence
    # when eta is introduced, instead of treating derived eta as independent.
    gamma = second if walk else nu*(2-second)
    if key == 'alpha': return 2-3*nu
    if key == 'beta': return (3*nu-gamma)/2
    if key == 'gamma': return gamma
    if key == 'delta': return (3*nu+gamma)/(3*nu-gamma)
    if key == 'nu': return nu
    if key == 'eta': return 2-gamma/nu
    raise KeyError(key)


def bounds(cls, key):
    inputs = INPUTS[cls]
    if key in inputs:
        return parse(inputs[key])
    if cls == 'saw' and key == 'omega':
        values = [d/n for d,n in product(parse(inputs['Delta1']), parse(inputs['nu']))]
    else:
        second = 'gamma' if cls == 'saw' else 'eta'
        values = [exponent_at(n,s,key,cls=='saw')
                  for n,s in product(parse(inputs['nu']),parse(inputs[second]))]
    # Every transform is monotone in each input on the specific positive nu,
    # gamma and 1+eta ranges here. checks.py verifies derivatives' signs and
    # denominator positivity before relying on corner extrema.
    return min(values), max(values)


def power10(n):
    return Q(10**n) if n >= 0 else Q(1,10**(-n))


def decimal_order(x):
    assert x > 0
    k = 0
    while x < power10(k): k -= 1
    while x >= power10(k+1): k += 1
    return k


def decimal_text(x, places):
    scale = 10**places
    integer = x*scale
    assert integer.denominator == 1
    integer = integer.numerator
    sign = '-' if integer < 0 else ''
    chars = str(abs(integer)).zfill(places+1)
    return sign+chars[:-places]+'.'+chars[-places:] if places else sign+chars


def rounded_ball(low, high):
    assert low < high
    mid = (low+high)/2
    step = power10(decimal_order((high-low)/2)-1)
    center = ((mid/step+Q(1,2)).numerator // (mid/step+Q(1,2)).denominator)*step
    needed = max(center-low, high-center)
    ratio = needed/step
    radius = (-(-ratio.numerator//ratio.denominator))*step
    # If rounding crossed a decade, recompute at the two-digit radius place.
    new_step = power10(decimal_order(radius)-1)
    if new_step > step:
        step = new_step
        ratio = mid/step+Q(1,2)
        center = (ratio.numerator//ratio.denominator)*step
        ratio = max(center-low,high-center)/step
        radius = (-(-ratio.numerator//ratio.denominator))*step
    places = max(0,-decimal_order(step))
    text = decimal_text(center,places)+' +/- '+decimal_text(radius,places)
    l,h = parse(text)
    assert l <= low < high <= h
    return text


def entry(cls, key):
    if key in INPUTS[cls]:
        value = INPUTS[cls][key]
        if cls == 'saw':
            ref = 'Clisby2017' if key == 'gamma' else 'ClisbyDuenweg2016'
            comment = 'Monte Carlo estimate, CITE{%s}.' % ref
        elif cls == 'ising' and key == 'omega':
            comment = r'$\omega=\Delta_{\epsilon^\prime}-3$, with $\Delta_{\epsilon^\prime}=3.82951(61)$ from the conditional bootstrap bounds in CITE{Reehorst2022}.'
        else:
            comment = 'Published estimate, CITE{%s}.' % SOURCES[cls]
        if cls == 'percolation' and key != 'omega':
            comment += ' The statistical and correction-fit uncertainties are added.'
        if cls == 'percolation' and key == 'omega':
            comment += r' This is the spatial correction exponent from that study; other determinations show substantial dispersion CITE{Borinsky2021}.'
    else:
        value = rounded_ball(*bounds(cls,key))
        ref = 'ClisbyDuenweg2016} and CITE{Clisby2017' if cls == 'saw' else SOURCES[cls]
        comment = 'Derived through the scaling relations from CITE{%s}; the radius encloses the input error box.' % ref
        if cls == 'saw' and key == 'omega':
            comment = r'Derived as $\omega=\Delta_1/\nu$ from $\Delta_1=0.528(8)$ and $\nu=0.58759700(40)$ in CITE{ClisbyDuenweg2016}.'
    return {'number':value,'comment':comment}


class CriticalExponents3D(numberdb.Generator):
    table = 'T472'
    parameters = ('class','exponent')
    type = 'R'
    rigour = 'measured'
    # The least precise entry (percolation omega) only supports one digit.
    # All strings retain their full, explicitly stated source precision.
    digits = 1
    files = ('generate.py','checks.py','check.py','source-notes.md','preflight-evidence.json')

    def enumerate(self):
        for cls in CLASSES:
            for exponent in EXPONENTS:
                yield {'class':cls,'exponent':exponent}

    def value(self, params, digits):
        return entry(params['class'],params['exponent'])


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        os.environ['NUMBERDB_API_KEY'] = sys.stdin.read().strip()
    gen = CriticalExponents3D()
    if '--fill-draft' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(gen.publish(message='Fill 35 sourced estimates and scaling-derived bounds; attach reproducible checks.'))
    else:
        report = gen.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)