Files
FESADev/tests/integration/analysis/thermal_oracle_generator.py
T

245 lines
10 KiB
Python

"""Generate test-only certified Gauss brackets from exact rational arithmetic.
Newton/Decimal produces candidates only. Exact Fraction signs prove n distinct
Legendre roots; rational interval recurrence encloses the associated weights.
No runtime solver dependency, reference modification, or libm ULP premise.
"""
from decimal import Decimal, localcontext
from fractions import Fraction as F
from pathlib import Path
import math
import re
import json
def add(a, b):
return a[0] + b[0], a[1] + b[1]
def neg(a):
return -a[1], -a[0]
def mul(a, b):
v = [x * y for x in a for y in b]
return min(v), max(v)
def div(a, b):
assert b[0] > 0 or b[1] < 0
return mul(a, (1 / b[1], 1 / b[0]))
def scale(a, b):
return mul(a, (F(b), F(b)))
def legendre(n, x):
p, q = (F(1), F(1)), x
for k in range(2, n + 1):
p, q = q, scale(add(scale(mul(x, q), 2 * k - 1), neg(scale(p, k - 1))), F(1, k))
derivative = div(scale(add(mul(x, q), neg(p)), n), add(mul(x, x), (-F(1), -F(1))))
return q, derivative
def bracket(value):
return math.nextafter(float(value[0]), -math.inf), math.nextafter(float(value[1]), math.inf)
def rule(n):
result = []
for i in range(n):
with localcontext() as context:
context.prec = 90
z = Decimal(str(math.cos(math.pi * (i + .75) / (n + .5))))
for _ in range(30):
p, q = Decimal(1), z
for k in range(2, n + 1):
p, q = q, ((2 * k - 1) * z * q - (k - 1) * p) / k
d = n * (z * q - p) / (z * z - 1)
z -= q / d
lo, hi = bracket((F(z), F(z)))
pl = legendre(n, (F(lo), F(lo)))[0][0]
ph = legendre(n, (F(hi), F(hi)))[0][0]
assert pl * ph < 0
x = F(lo), F(hi)
derivative = legendre(n, x)[1]
w = div((F(2), F(2)), mul(add((F(1), F(1)), neg(mul(x, x))), mul(derivative, derivative)))
result.append((lo, hi, *bracket(w)))
result.sort()
assert all(a[1] < b[0] for a, b in zip(result, result[1:]))
assert sum(F(r[2]) for r in result) < 2 < sum(F(r[3]) for r in result)
return result
def slab_fixtures():
# Approved Appendix A supplies the independently implemented Cholesky /
# Jacobi spectral candidates; Appendix E supplies certified transcendentals.
document = Path(__file__).resolve().parents[3] / "docs/shell-transient-heat-transfer/numerical-oracle-scratch.md"
blocks = re.findall(r"```python\n(.*?)```", document.read_text(encoding="utf-8-sig"), re.S)
spectral = {}
exec(blocks[0].split('out={"lambda"')[0], spectral)
interval_scope = {}
exec(blocks[-1].split("\nfor t in [.001")[0], interval_scope)
iv, ia, im, isc = (interval_scope[k] for k in ("interval", "add", "mul", "scale"))
ex = interval_scope["exneg"]
n = 4
def exact_inverse(matrix):
a = [list(row) + [F(i == j) for j in range(n)] for i, row in enumerate(matrix)]
for k in range(n):
pivot = next(i for i in range(k, n) if a[i][k])
a[k], a[pivot] = a[pivot], a[k]
divisor = a[k][k]
a[k] = [x / divisor for x in a[k]]
for i in range(n):
if i != k:
factor = a[i][k]
a[i] = [x - factor * y for x, y in zip(a[i], a[k])]
return [row[n:] for row in a]
def exact_mm(a, b):
return [[sum((x*y for x, y in zip(row, col)), F(0)) for col in zip(*b)] for row in a]
def matrix_multiply(a, b):
return [[sum_intervals([im(x, y) for x, y in zip(row, col)]) for col in zip(*b)] for row in a]
def sum_intervals(values):
total = iv(0)
for v in values:
total = ia(total, v)
return total
def apply(a, v):
return [sum_intervals([im(x, y) for x, y in zip(row, v)]) for row in a]
def power(a, count):
result = [[iv(i == j) for j in range(n)] for i in range(n)]
while count:
if count & 1:
result = matrix_multiply(result, a)
count >>= 1
if count:
a = matrix_multiply(a, a)
return result
c = [[F(0) for _ in range(5)] for _ in range(5)]
k = [[F(0) for _ in range(5)] for _ in range(5)]
for offset in (0, 2):
for i in range(3):
for j in range(3):
c[offset+i][offset+j] += F([[4, 2, -1], [2, 16, 2], [-1, 2, 4]][i][j], 60)
k[offset+i][offset+j] += F(2, 3)*[[7, -8, 1], [-8, 16, -8], [1, -8, 7]][i][j]
cf, kf = [r[1:] for r in c[1:]], [r[1:] for r in k[1:]]
inverse = exact_inverse(cf)
d = exact_mm(inverse, kf)
y = [-1-sum(inverse[i][j]*c[j+1][0] for j in range(n)) for i in range(n)]
def matrix_exponential(t):
# An independent enclosure of exp(-Cff^-1 Kff t) certifies all spectral
# eigenpair/coefficient errors together, rather than assuming libm ULPs.
# For ||B||inf<=1/2, the 64-term exponential remainder is bounded by
# 2*(1/2)^65/65!. Entrywise intervals preserve subsequent squarings.
magnitude = max(sum(abs(x*t) for x in row) for row in d)
shift = 0
while magnitude > F(1, 2):
magnitude /= 2
shift += 1
a = [[iv(-x*t/F(2)**shift) for x in row] for row in d]
term = [[iv(i == j) for j in range(n)] for i in range(n)]
result = [row[:] for row in term]
for order in range(1, 65):
term = [[isc(x, F(1, order)) for x in row] for row in matrix_multiply(term, a)]
result = [[ia(x, z) for x, z in zip(row, terms)] for row, terms in zip(result, term)]
remainder = 2*F(1, 2)**65/math.factorial(65)
result = [[ia(x, iv(-remainder, remainder)) for x in row] for row in result]
for _ in range(shift):
result = matrix_multiply(result, result)
return result
def outputs(error):
temperature = [iv(1)] + [ia(iv(1), z) for z in error]
derivative = [iv(0)] + apply([[iv(-x) for x in row] for row in d], error)
flux = []
for offset, s in ((0, -1), (0, 0), (0, 1), (2, -1), (2, 0), (2, 1)):
deriv = [F(4)*F(s-F(1, 2)), F(-8*s), F(4)*F(s+F(1, 2))]
flux.append(sum_intervals([isc(temperature[offset+i], -deriv[i]) for i in range(3)]))
energy = sum_intervals([isc(temperature[j], sum(c[i][j] for i in range(5))) for j in range(5)])
reaction = sum_intervals([isc(temperature[j], k[0][j]) for j in range(5)] +
[isc(derivative[j], c[0][j]) for j in range(5)])
return temperature + flux + [energy, reaction]
def spectral_candidate(t, count):
amplitudes = []
for eigenvalue, coefficient in zip(spectral["lam"], spectral["coef"]):
decay = ex(iv(F(eigenvalue)*t)) if count is None else power_scalar(
interval_scope["div"](iv(1), iv(1+F(eigenvalue)*t/count)), count)
amplitudes.append(isc(decay, F(coefficient)))
return outputs(apply([[iv(F(x)) for x in row] for row in spectral["W"]], amplitudes))
def power_scalar(x, count):
result = iv(1)
while count:
if count & 1:
result = im(result, x)
count >>= 1
if count:
x = im(x, x)
return result
table = [[.141, 10.7, .0304, 10.9], [.0153, 1.28, .00468, .824],
[.00598, .253, .00130, .00632], [.00139, .136, .000666, .00953],
[.000142, .0284, .0000945, .000526], [.0000414, .00816, .0000159, .0000479]]
fixtures, metrics = [], []
for time, space in zip((.001, .01, .05, .1, .5, 1.), table):
t = F(time)
temperatures, flux, energy, _, _ = interval_scope["series_interval"](time)
continuum = temperatures + [flux[i] for i in (0, 1, 2, 2, 3, 4)] + [energy, flux[0]]
solutions = []
spectral_enclosure = 0.
for count in (None, 4096, 8192):
if count is None:
operator = matrix_exponential(t)
else:
a = [[cf[i][j]+t/count*kf[i][j] for j in range(n)] for i in range(n)]
operator = power([[iv(x) for x in row] for row in exact_mm(exact_inverse(a), cf)], count)
exact = outputs(apply(operator, [iv(x) for x in y]))
candidate = spectral_candidate(t, count)
enclosed = [(min(a[0], b[0]), max(a[1], b[1])) for a, b in zip(exact, candidate)]
spectral_enclosure = max(spectral_enclosure, max(float(hi-lo) for lo, hi in enclosed))
solutions.append(enclosed)
fixtures.append((time, space, continuum, *solutions))
metrics.append({"time": time, "spectral_and_matrix_function_enclosure": spectral_enclosure})
print(json.dumps({"eigenvalues": spectral["lam"], "slab_oracle_metrics": metrics}, indent=2))
return fixtures
if __name__ == "__main__":
output = ["// Generated by thermal_oracle_generator.py; exact root/weight certificates.",
"#ifndef FESA_TESTS_THERMAL_GAUSS_ORACLE_H_", "#define FESA_TESTS_THERMAL_GAUSS_ORACLE_H_",
"#include <array>", "namespace thermal_oracle {",
"struct GaussPoint { double x_lo,x_hi,w_lo,w_hi; };"]
for n in (12, 16):
rows = rule(n)
output.append(f"inline constexpr std::array<GaussPoint,{n}> kGauss{n}{{{{")
output.extend(" {" + ",".join(x.hex() for x in row) + "}," for row in rows)
output.append("}};")
output.extend(["}", "#endif", ""])
destination = Path(__file__).with_name("thermal_gauss_oracle.h")
destination.write_text("\n".join(output), encoding="utf-8")
print("Certified Legendre roots/weights: 12 + 16 distinct roots;", destination.name)
fixtures = slab_fixtures()
output = ["// Generated by thermal_oracle_generator.py. Outward binary64 brackets.",
"#ifndef FESA_TESTS_THERMAL_SLAB_ORACLE_H_", "#define FESA_TESTS_THERMAL_SLAB_ORACLE_H_",
"#include <array>", "namespace thermal_oracle {",
"struct Bracket { double lo,hi; };",
"struct SlabCase { double time; std::array<double,4> space; std::array<Bracket,13> fourier,continuous,be4096,be8192; };",
"inline constexpr std::array<SlabCase,6> kSlab{{"]
for time, space, *values in fixtures:
output.append("{" + time.hex() + ",{{" + ",".join(x.hex() for x in space) + "}},")
for rows in values:
output.append("{{" + ",".join("{" + ",".join(x.hex() for x in bracket(row)) + "}" for row in rows) + "}},")
output.append("},")
output.extend(["}};", "}", "#endif", ""])
Path(__file__).with_name("thermal_slab_oracle.h").write_text("\n".join(output), encoding="utf-8")