generate.py

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

6203 bytes, as of the version from 2026-09-20 02:27 (current). Recorded here, not run.

"""Values of the Gauss hypergeometric function 2F1(a,b;c;z) -- numberdb.org/T348

This generator fills T348 with real principal values of the Gauss
hypergeometric function for the rational grid stated in the table. Exact
rational rows are omitted.

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
"""

import os
import sys

import numberdb.sage as numberdb
from sage.rings.complex_arb import ComplexBallField
from sage.rings.rational_field import QQ


TABLE = os.environ.get("NUMBERDB_TABLE", "T348")
DIGITS = 100
WORKING_GUARD = 64

A_VALUES = (QQ(-1) / 2, QQ(1) / 2, QQ(1), QQ(3) / 2, QQ(2), QQ(3))
C_VALUES = (QQ(1) / 2, QQ(1), QQ(3) / 2, QQ(2), QQ(3))
Z_VALUES = (
    QQ(-4),
    QQ(-2),
    QQ(-3) / 2,
    QQ(-1),
    QQ(-3) / 4,
    QQ(-1) / 2,
    QQ(-1) / 4,
    QQ(1) / 4,
    QQ(1) / 2,
)

ISOLATED_RATIONAL_ROWS = {
    (QQ(1) / 2, QQ(2), QQ(3), QQ(-2)),
    (QQ(2), QQ(2), QQ(1) / 2, QQ(-3) / 2),
    (QQ(2), QQ(2), QQ(3) / 2, QQ(-1) / 2),
    (QQ(2), QQ(3), QQ(1) / 2, QQ(-3) / 4),
    (QQ(2), QQ(3), QQ(3) / 2, QQ(-1) / 4),
}


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


def _field(digits, guard=WORKING_GUARD):
    return ComplexBallField(numberdb.bits(digits, losing=guard))


def _is_integer(x):
    return QQ(x).denominator() == 1


def _is_nonpositive_integer(x):
    return _is_integer(x) and QQ(x) <= 0


def _is_rational_row(a, b, c, z):
    """Rows whose value is known to be rational and is intentionally omitted."""
    if z == 0:
        return True
    if _is_nonpositive_integer(a) or _is_nonpositive_integer(b):
        return True
    for upper, other in ((a, b), (b, a)):
        if z == -1 and c == 1 + upper - other:
            if _is_nonpositive_integer(1 + upper / 2 - other):
                return True
        if z == QQ(1) / 2 and upper + other == 1:
            if _is_nonpositive_integer((upper + c) / 2):
                return True
            if _is_nonpositive_integer((c - upper + 1) / 2):
                return True
        if c - other == -1 and z != 1 and z / (z - 1) == c / upper:
            return True
        if _is_nonpositive_integer(c - other) and _is_integer(upper):
            return True
    if (a, b, c, z) in ISOLATED_RATIONAL_ROWS:
        return True
    return False


def _ordered_pair(a, b):
    return (a, b) if a <= b else (b, a)


def _real(value, label):
    if not value.real().is_finite() or not value.imag().is_finite():
        raise ArithmeticError("computed a non-finite ball for %s: %s" % (label, value))
    if not value.imag().contains_zero():
        raise ArithmeticError("expected a real value for %s: %s" % (label, value))
    return value.real()


def hypergeometric_2f1(a, b, c, z, digits):
    field = _field(digits)
    value = field(z).hypergeometric([field(a), field(b)], [field(c)])
    return _real(value, "2F1(%s,%s;%s;%s)" % (a, b, c, z))


class GaussHypergeometricValues(numberdb.Generator):

    table = TABLE
    parameters = ("a", "b", "c", "z")
    type = "R"
    digits = DIGITS
    rigour = "proven"

    def enumerate(self):
        for a in A_VALUES:
            for b in A_VALUES:
                if b < a:
                    continue
                for c in C_VALUES:
                    for z in Z_VALUES:
                        if _is_rational_row(a, b, c, z):
                            continue
                        yield {"a": str(a), "b": str(b), "c": str(c), "z": str(z)}

    def value(self, params, digits):
        a = QQ(params["a"])
        b = QQ(params["b"])
        c = QQ(params["c"])
        z = QQ(params["z"])
        return hypergeometric_2f1(a, b, c, z, digits)


def fill_draft_once(generator, message):
    """Fill a fresh draft without the 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,
            assisted_by=os.environ.get("NUMBERDB_ASSISTED_BY", "codex-cli"),
        ),
        upsert=False,
        run=run,
        rigour=generator.rigour,
    )

    files = _source_files(generator)
    stored = []
    for name, body in sorted(files.items()):
        attach(table, name, body, run=run, message=message,
               rigour=generator.rigour)
        stored.append(name)

    return {
        "tid": answer.get("tid", table),
        "revision": answer.get("revision"),
        "entries": len(entries),
        "files": stored,
    }


if __name__ == "__main__":
    _key_from_stdin()
    generator = GaussHypergeometricValues()
    if os.environ.get("NUMBERDB_PUBLISH") == "1" or "--publish" in sys.argv:
        print(fill_draft_once(
            generator,
            message="fill Gauss hypergeometric values in ball arithmetic"))
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)