back to table · edit · history · where entries came from · files · download
6146 bytes, as of the version from 2026-10-28 21:43 (current). Recorded here, not run.
"""Fisher zeros of the Ising model on finite rectangles with free boundaries -- numberdb.org/T591.
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
sage -python checks.py # independent polynomial checks
Writing requires NUMBERDB_API_KEY. --fill writes entries; it does not publish
or offer the draft. The campaign wrapper may instead supply the key on stdin
with NUMBERDB_KEY_FROM_STDIN=1 and set NUMBERDB_PUBLISH=1.
The integer row transfer builds F(b)=sum_S b^d(S). Exact algebraic roots are
isolated by Sage, squarefree decomposition preserves their multiplicities,
and comparisons of algebraic real and imaginary parts fix the ordering.
Each root is converted directly from QQbar to a certified complex ball.
The 96-bit guard is measured in checks.py, including each nonzero component.
The finite geometry selection is explained in argument-selection.md.
"""
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from sage.rings.polynomial.complex_roots import complex_roots
from sage.rings.complex_arb import ComplexBallField
from sage.rings.qqbar import QQbar, _init_qqbar
from numberdb import ComplexInterval, RealInterval
from fractions import Fraction
from functools import lru_cache, cmp_to_key
from sage.rings.complex_interval_field import ComplexIntervalField
import os
import sys
_init_qqbar()
R = PolynomialRing(ZZ, 'b')
b = R.gen()
MAX_SIDE = 6
WORKING_GUARD = 96
def rectangles(max_side=MAX_SIDE):
for m in range(2, max_side + 1):
for n in range(m, max_side + 1):
yield m, n
@lru_cache(maxsize=None)
def partition_polynomial(m, n):
"""Integer row transfer, with m vertices in each of n rows."""
states = range(1 << m)
horizontal = [sum(((s >> k) ^ (s >> (k + 1))) & 1
for k in range(m - 1)) for s in states]
current = [b ** horizontal[s] for s in states]
powers = [b ** k for k in range(2 * m)]
for row in range(1, n):
current = [sum((current[s] * powers[(s ^ t).bit_count() + horizontal[t]]
for s in states), R.zero()) for t in states]
return sum(current, R.zero())
def compare_roots(left, right):
"""Exact lexicographic order, recognizing conjugates before real parts.
Disjoint outward-rounded intervals certify unequal coordinates. Conjugate
roots have exactly equal real parts, so their imaginary parts decide.
Testing algebraic equality of two independently constructed real parts
first can unnecessarily construct a large splitting field.
"""
a, c = left[0], right[0]
field = ComplexIntervalField(160)
ai, ci = field(a), field(c)
if ai.real() < ci.real():
return -1
if ai.real() > ci.real():
return 1
if a.conjugate() == c:
return -1 if a.imag() < c.imag() else (1 if a.imag() > c.imag() else 0)
if a == -a.conjugate() and c == -c.conjugate():
return -1 if a.imag() < c.imag() else (1 if a.imag() > c.imag() else 0)
ar, cr = a.real(), c.real()
if ar != cr:
return -1 if ar < cr else 1
return -1 if a.imag() < c.imag() else (1 if a.imag() > c.imag() else 0)
@lru_cache(maxsize=None)
def roots(m, n):
p = partition_polynomial(m, n)
values = complex_roots(p, retval='algebraic')
values.sort(key=cmp_to_key(compare_roots))
assert sum(e for r, e in values) == p.degree()
return tuple(values)
def root_ball(m, n, j, digits=100):
root = roots(m, n)[j - 1][0]
# Direct conversion of an exact algebraic number encloses the root.
# QQbar also proves axis membership, preserving exact zero components.
field = ComplexBallField(numberdb.bits(digits, losing=WORKING_GUARD))
value = field(root)
# Axis membership follows from exact conjugation identities, not small digits.
if root == root.conjugate():
value = field(value.real(), 0)
if root == -root.conjugate():
value = field(0, value.imag())
return value
def as_fraction(endpoint):
q = endpoint.exact_rational()
return Fraction(int(q.numerator()), int(q.denominator()))
class FisherZeros(numberdb.Generator):
table = 'T591'
parameters = ('m', 'n', 'j')
type = 'C'
digits = 100
rigour = 'proven'
def enumerate(self, max_side=MAX_SIDE):
for m, n in rectangles(max_side):
for j in range(1, len(roots(m, n)) + 1):
yield {'m': m, 'n': n, 'j': j}
def value(self, params, digits):
m, n, j = (int(params[k]) for k in self.parameters)
root, multiplicity = roots(m, n)[j - 1]
value = QQ(-1) if root == -1 else root_ball(m, n, j, digits)
if root != -1 and (value.real().is_zero() or value.imag().is_zero()):
ci = ComplexIntervalField(numberdb.bits(digits, losing=WORKING_GUARD))(value)
parts = []
for part in (ci.real(), ci.imag()):
lo, hi = part.endpoints()
if lo == hi:
parts.append(as_fraction(lo))
else:
parts.append(RealInterval(as_fraction(lo), as_fraction(hi)))
value = ComplexInterval(*parts)
return {'number': value, 'comment': 'Multiplicity %s.' % multiplicity}
if __name__ == '__main__':
if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
token = sys.stdin.read().strip()
if '=' in token and token.split('=', 1)[0].strip().isidentifier():
token = token.split('=', 1)[1].strip().strip('\"\'')
os.environ['NUMBERDB_API_KEY'] = token
generator = FisherZeros()
if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
print(generator.publish(message='Fill all distinct Fisher zeros for 15 free rectangles'))
else:
report = generator.verify(sample=None)
print(report)
sys.exit(0 if report.ok else 1)