generate.py

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

7683 bytes, as of the version from 2026-09-21 12:51 (current). Recorded here, not run.

"""Discriminants of the depressed polynomial of degree n -- numberdb.org/T382

For n = 2, ..., 6 this stores the discriminant of

    f_n(x) = x^n + a_{n-2} x^{n-2} + ... + a_1 x + a_0.

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

The discriminant is computed from the exact resultant formula for a monic
polynomial. The integrity checks compare the rows with the root-product
definition on sum-zero roots, with the stored general-polynomial discriminants
where those rows exist, and with Sage's univariate discriminant at integer
specializations.
"""

import os
import sys
from itertools import combinations

import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing


TABLE = os.environ.get("NUMBERDB_TABLE", "T382")
UP_TO_N = 6
GENERAL_TABLE = "T381"


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 coefficient_ring(n):
    return PolynomialRing(ZZ, ["a%s" % i for i in range(n - 1)])


def discriminant_polynomial(n):
    coefficients = coefficient_ring(n).gens()
    base = coefficients[0].parent()
    poly_ring = PolynomialRing(base, "x")
    x = poly_ring.gen()
    f = x ** n + sum(coefficients[i] * x ** i for i in range(n - 1))
    sign = -1 if (n * (n - 1) // 2) % 2 else 1
    return sign * f.resultant(f.derivative())


def elementary_symmetric(values, k, ring):
    if k == 0:
        return ring(1)
    total = ring(0)
    for terms in combinations(values, k):
        product = ring(1)
        for term in terms:
            product *= term
        total += product
    return total


def root_product_polynomial(n):
    ring = PolynomialRing(ZZ, ["r%s" % i for i in range(n - 1)])
    roots = list(ring.gens())
    roots.append(-sum(roots))

    product = ring(1)
    for i, alpha in enumerate(roots):
        for beta in roots[i + 1:]:
            product *= (alpha - beta) ** 2
    return ring, roots, product


def check_root_product(n):
    ring, roots, product = root_product_polynomial(n)
    discriminant = discriminant_polynomial(n)
    images = [
        (-1) ** (n - i) * elementary_symmetric(roots, n - i, ring)
        for i in range(n - 1)
    ]
    mapped = discriminant.parent().hom(images, ring)(discriminant)
    if mapped != product:
        raise ArithmeticError("root-product check failed at n=%s" % n)


def check_small_formulas(values):
    ring2 = coefficient_ring(2)
    a0 = ring2.gens()[0]
    if values[2] != -4 * a0:
        raise ArithmeticError("quadratic formula check failed")

    ring3 = coefficient_ring(3)
    a0, a1 = ring3.gens()
    if values[3] != -4 * a1 ** 3 - 27 * a0 ** 2:
        raise ArithmeticError("cubic formula check failed")


def integer_samples(n):
    samples = []
    for seed in range(5):
        samples.append([
            ZZ(((i + 2) * (seed + 3) + seed * seed) % 9 - 4)
            for i in range(n - 1)
        ])
    samples.append([ZZ(0) for _ in range(n - 1)])
    return samples


def check_integer_specializations(values):
    poly_ring = PolynomialRing(ZZ, "x")
    x = poly_ring.gen()
    for n, discriminant in values.items():
        coefficient_parent = discriminant.parent()
        for sample in integer_samples(n):
            specialized = coefficient_parent.hom(sample, ZZ)(discriminant)
            f = x ** n + sum(sample[i] * x ** i for i in range(n - 1))
            expected = f.discriminant()
            if specialized != expected:
                raise ArithmeticError(
                    "integer specialization failed at n=%s, sample=%s"
                    % (n, sample)
                )


def check_against_general_table(values):
    try:
        general = numberdb.table(GENERAL_TABLE)
    except Exception as trouble:  # noqa: BLE001
        print("skipped stored T381 comparison: %s" % trouble)
        return

    for n in range(2, min(5, UP_TO_N) + 1):
        text = general["Numbers"][str(n)]
        general_ring = PolynomialRing(ZZ, ["a%s" % i for i in range(n + 1)])
        general_discriminant = general_ring(text)
        target_ring = values[n].parent()
        target_gens = list(target_ring.gens())
        images = target_gens + [target_ring(0), target_ring(1)]
        specialized = general_ring.hom(images, target_ring)(general_discriminant)
        if specialized != values[n]:
            raise ArithmeticError("stored T381 specialization failed at n=%s" % n)


def run_integrity_checks(up_to_n=UP_TO_N):
    values = {n: discriminant_polynomial(n) for n in range(2, up_to_n + 1)}
    check_small_formulas(values)
    for n in values:
        check_root_product(n)
    check_integer_specializations(values)
    check_against_general_table(values)
    longest = max((len(str(value)), n) for n, value in values.items())
    print("integrity checks passed for depressed discriminants n=2..%s" % up_to_n)
    print("longest stored discriminant has %d characters at n=%s" % longest)
    return values


class DepressedPolynomialDiscriminants(numberdb.Generator):

    table = TABLE
    parameters = ("n",)
    type = "Z[]"
    rigour = "exact"

    def enumerate(self, up_to_n=UP_TO_N):
        for n in range(2, up_to_n + 1):
            yield {"n": str(n)}

    def value(self, params, digits):
        return {"number": discriminant_polynomial(int(params["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 = DepressedPolynomialDiscriminants()
    run_integrity_checks()
    if "--publish" in sys.argv or os.environ.get("NUMBERDB_PUBLISH") == "1":
        print(fill_draft_once(
            generator,
            message="fill depressed polynomial discriminants from exact resultants",
        ))
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)