619 lines
20 KiB
Python
619 lines
20 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Cubic windows for the Appendix D maps that were still cleartext-only.
|
||
|
|
|
||
|
|
Sollya builds a longest-feasible partition whose fixed-point cubic stays
|
||
|
|
within half an ulp. Mathematica (Remez), Maple (numapprox[minimax]), and
|
||
|
|
MATLAB/Chebfun (minimax) each fit the same pieces. The shipped cubic is
|
||
|
|
the one with the smallest absolute error after rounding coefficients to
|
||
|
|
2^{-(k+16)}.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import math
|
||
|
|
import re
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from decimal import Decimal, ROUND_HALF_UP
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
PREC = (8, 12, 16, 20, 24, 28, 32)
|
||
|
|
SOLLYA = "sollya"
|
||
|
|
MATH = "/home/dawkteraitch/Wolfram/15.0/Executables/math"
|
||
|
|
MAPLE = "/home/dawkteraitch/maple2025/bin/maple"
|
||
|
|
MATLAB = "/home/dawkteraitch/MATLAB/R2026a/bin/matlab"
|
||
|
|
WORK = Path("/tmp/cas-lic/window")
|
||
|
|
EXPR = {
|
||
|
|
"lecun": "(17159/10000) * tanh((2 * x) / 3)",
|
||
|
|
"hard": "expm1(x) * (x + 1) / 2",
|
||
|
|
}
|
||
|
|
NUMBER = re.compile(r"[+-]?(?:\d+\.\d*|\d+\.?\d*)(?:[eE][+-]?\d+)?")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Piece:
|
||
|
|
left: int
|
||
|
|
right: int
|
||
|
|
coeffs: list[int] # c0..c3 at scale 2^q
|
||
|
|
err: float
|
||
|
|
source: str
|
||
|
|
|
||
|
|
|
||
|
|
class Sollya:
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self.proc: subprocess.Popen[str] | None = None
|
||
|
|
self.start()
|
||
|
|
|
||
|
|
def start(self) -> None:
|
||
|
|
if self.proc is not None:
|
||
|
|
self.proc.kill()
|
||
|
|
self.proc.wait()
|
||
|
|
self.proc = subprocess.Popen(
|
||
|
|
["stdbuf", "-o0", "-e0", SOLLYA],
|
||
|
|
stdin=subprocess.PIPE,
|
||
|
|
stdout=subprocess.PIPE,
|
||
|
|
stderr=subprocess.STDOUT,
|
||
|
|
text=True,
|
||
|
|
bufsize=1,
|
||
|
|
)
|
||
|
|
self._run("verbosity = 0;\nprec = 120;\n", "READY")
|
||
|
|
|
||
|
|
def close(self) -> None:
|
||
|
|
if self.proc is not None:
|
||
|
|
self.proc.kill()
|
||
|
|
self.proc.wait()
|
||
|
|
self.proc = None
|
||
|
|
|
||
|
|
def _run(self, script: str, sentinel: str) -> list[str]:
|
||
|
|
assert self.proc is not None and self.proc.stdin and self.proc.stdout
|
||
|
|
self.proc.stdin.write(script)
|
||
|
|
self.proc.stdin.write(f'print("{sentinel}");\n')
|
||
|
|
self.proc.stdin.flush()
|
||
|
|
lines: list[str] = []
|
||
|
|
while True:
|
||
|
|
line = self.proc.stdout.readline()
|
||
|
|
if line == "":
|
||
|
|
raise RuntimeError("sollya exited")
|
||
|
|
if sentinel in line:
|
||
|
|
return lines
|
||
|
|
lines.append(line.rstrip("\n"))
|
||
|
|
|
||
|
|
def fit(
|
||
|
|
self, name: str, left: int, right: int, k: int, q: int, tol: float
|
||
|
|
) -> tuple[float, list[int]] | None:
|
||
|
|
# fpminimax is ill-conditioned on a tiny interval at 0 and can report
|
||
|
|
# an error above half an ulp for a one-ulp step. Taylor is the
|
||
|
|
# fallback, and the search keeps the smaller certified error.
|
||
|
|
mini = self._polynomial(name, left, right, k, q, taylor=False)
|
||
|
|
if mini is not None and mini[0] <= tol:
|
||
|
|
return mini
|
||
|
|
tay = self._polynomial(name, left, right, k, q, taylor=True)
|
||
|
|
cands = [item for item in (mini, tay) if item is not None]
|
||
|
|
if not cands:
|
||
|
|
return None
|
||
|
|
return min(cands, key=lambda item: item[0])
|
||
|
|
|
||
|
|
def _polynomial(
|
||
|
|
self, name: str, left: int, right: int, k: int, q: int, taylor: bool
|
||
|
|
) -> tuple[float, list[int]] | None:
|
||
|
|
expr = EXPR[name]
|
||
|
|
if taylor:
|
||
|
|
# taylor(f, n, t) returns p such that p(x-t) is the expansion at t.
|
||
|
|
build = (
|
||
|
|
f"t0 = (({left})+({right}))/2 * 2^(-{k});\n"
|
||
|
|
f"pshift = taylor({expr}, 3, t0);\n"
|
||
|
|
f"p = pshift(x - t0);"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
build = (
|
||
|
|
f"p = fpminimax({expr}, 3, [|80,80,80,80|], "
|
||
|
|
f"[{left}*2^(-{k}), {right}*2^(-{k})], absolute);"
|
||
|
|
)
|
||
|
|
script = f"""
|
||
|
|
{build}
|
||
|
|
qbits = {q};
|
||
|
|
i0 = nearestint(coeff(p, 0) * 2^qbits);
|
||
|
|
i1 = nearestint(coeff(p, 1) * 2^qbits);
|
||
|
|
i2 = nearestint(coeff(p, 2) * 2^qbits);
|
||
|
|
i3 = nearestint(coeff(p, 3) * 2^qbits);
|
||
|
|
pr = i0*2^(-qbits) + i1*2^(-qbits)*x + i2*2^(-qbits)*x^2 + i3*2^(-qbits)*x^3;
|
||
|
|
e = dirtyinfnorm(({expr}) - pr, [{left}*2^(-{k}), {right}*2^(-{k})]);
|
||
|
|
print(i0);
|
||
|
|
print(i1);
|
||
|
|
print(i2);
|
||
|
|
print(i3);
|
||
|
|
print(e);
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
lines = self._run(script, "FITDONE")
|
||
|
|
except RuntimeError:
|
||
|
|
self.start()
|
||
|
|
return None
|
||
|
|
if any("Error" in line or "error" in line for line in lines):
|
||
|
|
self.start()
|
||
|
|
return None
|
||
|
|
nums: list[str] = []
|
||
|
|
for line in lines:
|
||
|
|
nums.extend(NUMBER.findall(line))
|
||
|
|
if len(nums) < 5:
|
||
|
|
self.start()
|
||
|
|
return None
|
||
|
|
coeffs = [int(Decimal(nums[i]).to_integral_value(ROUND_HALF_UP)) for i in range(4)]
|
||
|
|
err = max(float(n) for n in nums[4:])
|
||
|
|
if not math.isfinite(err):
|
||
|
|
return None
|
||
|
|
return err, coeffs
|
||
|
|
|
||
|
|
def score(self, name: str, left: int, right: int, k: int, q: int, coeffs: list[int]) -> float | None:
|
||
|
|
expr = EXPR[name]
|
||
|
|
c0, c1, c2, c3 = coeffs
|
||
|
|
script = f"""
|
||
|
|
pr = ({c0})*2^(-{q}) + ({c1})*2^(-{q})*x + ({c2})*2^(-{q})*x^2 + ({c3})*2^(-{q})*x^3;
|
||
|
|
e = dirtyinfnorm(({expr}) - pr, [{left}*2^(-{k}), {right}*2^(-{k})]);
|
||
|
|
print(e);
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
lines = self._run(script, "SCOREDONE")
|
||
|
|
except RuntimeError:
|
||
|
|
self.start()
|
||
|
|
return None
|
||
|
|
if any("Error" in line or "error" in line for line in lines):
|
||
|
|
self.start()
|
||
|
|
return None
|
||
|
|
nums: list[str] = []
|
||
|
|
for line in lines:
|
||
|
|
nums.extend(NUMBER.findall(line))
|
||
|
|
if not nums:
|
||
|
|
return None
|
||
|
|
err = max(float(n) for n in nums)
|
||
|
|
return err if math.isfinite(err) else None
|
||
|
|
|
||
|
|
|
||
|
|
def half_ulp(k: int) -> float:
|
||
|
|
return 2.0 ** (-(k + 1))
|
||
|
|
|
||
|
|
|
||
|
|
def q_of(k: int) -> int:
|
||
|
|
return k + 16
|
||
|
|
|
||
|
|
|
||
|
|
def quantize(coeffs: list[str], q: int) -> list[int]:
|
||
|
|
scale = Decimal(2) ** q
|
||
|
|
out: list[int] = []
|
||
|
|
for raw in coeffs:
|
||
|
|
value = Decimal(raw) * scale
|
||
|
|
if value >= 0:
|
||
|
|
out.append(int(value.to_integral_value(ROUND_HALF_UP)))
|
||
|
|
else:
|
||
|
|
out.append(-int((-value).to_integral_value(ROUND_HALF_UP)))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def lecun_tail(k: int) -> int:
|
||
|
|
num = 17159 * (1 << k)
|
||
|
|
den = 10000
|
||
|
|
return (num + den // 2) // den
|
||
|
|
|
||
|
|
|
||
|
|
def lecun_end(k: int) -> int:
|
||
|
|
"""First raw at which the constant tail is within 1/4 ulp of the asymptote gap."""
|
||
|
|
tail = lecun_tail(k) / 2**k
|
||
|
|
# f increases to 1.7159. Leave a quarter ulp for rounding the tail itself.
|
||
|
|
gap = 0.25 * (2 ** (-k))
|
||
|
|
target = 1.7159 - gap
|
||
|
|
lo, hi = 0.0, 40.0
|
||
|
|
for _ in range(80):
|
||
|
|
mid = (lo + hi) / 2
|
||
|
|
if 1.7159 * math.tanh(2 * mid / 3) >= target:
|
||
|
|
hi = mid
|
||
|
|
else:
|
||
|
|
lo = mid
|
||
|
|
return max(1, math.ceil(hi * 2**k - 1e-9))
|
||
|
|
|
||
|
|
|
||
|
|
def march(sollya: Sollya, name: str, start: int, end: int, k: int) -> list[Piece]:
|
||
|
|
q = q_of(k)
|
||
|
|
tol = half_ulp(k)
|
||
|
|
pieces: list[Piece] = []
|
||
|
|
left = start
|
||
|
|
guess = 1
|
||
|
|
while left < end:
|
||
|
|
step = max(1, guess)
|
||
|
|
last: tuple[int, float, list[int]] | None = None
|
||
|
|
fail: int | None = None
|
||
|
|
while left + step <= end:
|
||
|
|
got = sollya.fit(name, left, left + step, k, q, tol)
|
||
|
|
if got is not None and got[0] <= tol:
|
||
|
|
last = (left + step, got[0], got[1])
|
||
|
|
step *= 2
|
||
|
|
else:
|
||
|
|
fail = min(end, left + step)
|
||
|
|
break
|
||
|
|
else:
|
||
|
|
if last is None or last[0] != end:
|
||
|
|
got = sollya.fit(name, left, end, k, q, tol)
|
||
|
|
if got is not None and got[0] <= tol:
|
||
|
|
last = (end, got[0], got[1])
|
||
|
|
fail = None
|
||
|
|
else:
|
||
|
|
fail = end
|
||
|
|
if fail is None and last is not None:
|
||
|
|
right, err, coeffs = last
|
||
|
|
else:
|
||
|
|
lo = last[0] if last is not None else left
|
||
|
|
hi = fail if fail is not None else end
|
||
|
|
best = last
|
||
|
|
while hi - lo > 1:
|
||
|
|
mid = (lo + hi) // 2
|
||
|
|
got = sollya.fit(name, left, mid, k, q, tol)
|
||
|
|
if got is not None and got[0] <= tol:
|
||
|
|
best = (mid, got[0], got[1])
|
||
|
|
lo = mid
|
||
|
|
else:
|
||
|
|
hi = mid
|
||
|
|
if best is None:
|
||
|
|
got = sollya.fit(name, left, left + 1, k, q, tol)
|
||
|
|
if got is None:
|
||
|
|
raise RuntimeError(f"{name} k={k} no cubic on [{left}, {left+1}]")
|
||
|
|
best = (left + 1, got[0], got[1])
|
||
|
|
right, err, coeffs = best
|
||
|
|
pieces.append(Piece(left, right, coeffs, err, "sollya"))
|
||
|
|
guess = max(1, right - left)
|
||
|
|
left = right
|
||
|
|
print(f" {name} k={k} pieces={len(pieces)} span={right - pieces[-1].left} err={err:.3e}", flush=True)
|
||
|
|
return pieces
|
||
|
|
|
||
|
|
|
||
|
|
def write_intervals(pieces_by_key: dict[tuple[str, int], list[Piece]], path: Path) -> None:
|
||
|
|
with path.open("w") as handle:
|
||
|
|
for (name, k), pieces in pieces_by_key.items():
|
||
|
|
q = q_of(k)
|
||
|
|
for piece in pieces:
|
||
|
|
handle.write(f"{name} {k} {piece.left} {piece.right} {q}\n")
|
||
|
|
|
||
|
|
|
||
|
|
def run_maple(interval_path: Path, out_path: Path) -> None:
|
||
|
|
program = WORK / "maple_batch.mpl"
|
||
|
|
program.write_text(
|
||
|
|
f"""
|
||
|
|
Digits := 40:
|
||
|
|
with(numapprox):
|
||
|
|
fd := fopen("{out_path}", WRITE):
|
||
|
|
infd := fopen("{interval_path}", READ):
|
||
|
|
while true do
|
||
|
|
line := readline(infd);
|
||
|
|
if line = 0 then break; end if;
|
||
|
|
parts := sscanf(line, "%s %d %d %d %d");
|
||
|
|
gname := parts[1];
|
||
|
|
a := parts[3] * 2^(-parts[2]);
|
||
|
|
b := parts[4] * 2^(-parts[2]);
|
||
|
|
try
|
||
|
|
if gname = "lecun" then
|
||
|
|
p := minimax(17159/10000*tanh(2*x/3), x = a .. b, [3, 0]);
|
||
|
|
else
|
||
|
|
p := minimax((exp(x)-1)*(x+1)/2, x = a .. b, [3, 0]);
|
||
|
|
end if;
|
||
|
|
p := expand(p);
|
||
|
|
fprintf(fd, "%.25e %.25e %.25e %.25e\\n",
|
||
|
|
evalf(coeff(p, x, 0)), evalf(coeff(p, x, 1)),
|
||
|
|
evalf(coeff(p, x, 2)), evalf(coeff(p, x, 3)));
|
||
|
|
catch:
|
||
|
|
fprintf(fd, "FAIL\\n");
|
||
|
|
end try;
|
||
|
|
end do:
|
||
|
|
fclose(fd):
|
||
|
|
fclose(infd):
|
||
|
|
quit:
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
subprocess.run([MAPLE, "-q", str(program)], check=True, timeout=3600)
|
||
|
|
|
||
|
|
|
||
|
|
def run_math(interval_path: Path, out_path: Path) -> None:
|
||
|
|
program = WORK / "mma_batch.wl"
|
||
|
|
program.write_text(
|
||
|
|
r"""
|
||
|
|
pow[base_, 0] := 1;
|
||
|
|
pow[base_, n_Integer] := base^n;
|
||
|
|
lecun[x_] := 17159/10000*Tanh[2*x/3];
|
||
|
|
hard[x_] := (Exp[x] - 1)*(x + 1)/2;
|
||
|
|
remez[ff_, a_, b_] := Module[
|
||
|
|
{n = 3, m = 5, mid, half, nodes, coef, mat, rhs, sol, grid, err, idx, drop, tpoly, xpoly},
|
||
|
|
mid = N[(a + b)/2, 40];
|
||
|
|
half = N[(b - a)/2, 40];
|
||
|
|
nodes = Sort[Table[N[Cos[Pi*(k + 1/2)/m], 40], {k, 0, m - 1}]];
|
||
|
|
coef = {0, 0, 0, 0};
|
||
|
|
Do[
|
||
|
|
mat = N[Table[If[j <= n + 1, pow[nodes[[i]], j - 1], -(-1)^i], {i, m}, {j, m}], 40];
|
||
|
|
rhs = N[Table[ff[mid + half*nodes[[i]]], {i, m}], 40];
|
||
|
|
sol = Quiet[Check[LinearSolve[mat, rhs], $Failed]];
|
||
|
|
If[sol === $Failed || Head[sol] =!= List, Break[]];
|
||
|
|
coef = N[sol[[1 ;; n + 1]], 40];
|
||
|
|
grid = N[Subdivide[-1, 1, 256], 40];
|
||
|
|
err = Table[
|
||
|
|
coef[[1]] + coef[[2]]*grid[[i]] + coef[[3]]*grid[[i]]^2 + coef[[4]]*grid[[i]]^3
|
||
|
|
- ff[mid + half*grid[[i]]],
|
||
|
|
{i, Length[grid]}];
|
||
|
|
idx = {1, Length[grid]};
|
||
|
|
Do[
|
||
|
|
If[Abs[err[[i]]] >= Abs[err[[i - 1]]] && Abs[err[[i]]] >= Abs[err[[i + 1]]],
|
||
|
|
AppendTo[idx, i]],
|
||
|
|
{i, 2, Length[grid] - 1}];
|
||
|
|
While[Length[idx] > m,
|
||
|
|
drop = Ordering[Abs[err[[idx]]], 1][[1]];
|
||
|
|
idx = Delete[idx, drop];
|
||
|
|
];
|
||
|
|
If[Length[idx] == m, nodes = Sort[N[grid[[idx]], 40]]];
|
||
|
|
, {8}];
|
||
|
|
tpoly = coef[[1]] + coef[[2]]*((x - mid)/half) + coef[[3]]*((x - mid)/half)^2
|
||
|
|
+ coef[[4]]*((x - mid)/half)^3;
|
||
|
|
xpoly = Expand[N[tpoly, 30]];
|
||
|
|
Table[N[Coefficient[xpoly, x, k], 20], {k, 0, 3}]
|
||
|
|
];
|
||
|
|
lines = ReadList["""
|
||
|
|
+ f'"{interval_path}"'
|
||
|
|
+ r""", String];
|
||
|
|
out = OpenWrite["""
|
||
|
|
+ f'"{out_path}"'
|
||
|
|
+ r"""];
|
||
|
|
Do[
|
||
|
|
parts = StringSplit[line];
|
||
|
|
If[Length[parts] < 5, Continue[]];
|
||
|
|
name = parts[[1]];
|
||
|
|
kk = ToExpression[parts[[2]]];
|
||
|
|
left = ToExpression[parts[[3]]];
|
||
|
|
right = ToExpression[parts[[4]]];
|
||
|
|
aa = N[left*2^(-kk), 30];
|
||
|
|
bb = N[right*2^(-kk), 30];
|
||
|
|
ff = If[name == "lecun", lecun, hard];
|
||
|
|
cs = Quiet[Check[remez[ff, aa, bb], $Failed]];
|
||
|
|
If[cs === $Failed || Head[cs] =!= List,
|
||
|
|
WriteString[out, "FAIL\n"],
|
||
|
|
WriteString[out, StringRiffle[Map[ToString[CForm[#]] &, cs], " "], "\n"]
|
||
|
|
];
|
||
|
|
, {line, lines}];
|
||
|
|
Close[out];
|
||
|
|
Exit[];
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
subprocess.run([MATH, "-script", str(program)], check=True, timeout=3600)
|
||
|
|
|
||
|
|
|
||
|
|
def run_matlab(interval_path: Path, out_path: Path) -> None:
|
||
|
|
program = WORK / "matlab_batch.m"
|
||
|
|
program.write_text(
|
||
|
|
f"""
|
||
|
|
fid = fopen('{interval_path}', 'r');
|
||
|
|
out = fopen('{out_path}', 'w');
|
||
|
|
while true
|
||
|
|
line = fgetl(fid);
|
||
|
|
if ~ischar(line), break; end
|
||
|
|
parts = strsplit(strtrim(line));
|
||
|
|
if numel(parts) < 5, continue; end
|
||
|
|
name = parts{{1}};
|
||
|
|
kk = str2double(parts{{2}});
|
||
|
|
left = str2double(parts{{3}});
|
||
|
|
right = str2double(parts{{4}});
|
||
|
|
aa = left * 2^(-kk);
|
||
|
|
bb = right * 2^(-kk);
|
||
|
|
try
|
||
|
|
if strcmp(name, 'lecun')
|
||
|
|
f = chebfun(@(x) 1.7159*tanh(2*x/3), [aa, bb]);
|
||
|
|
else
|
||
|
|
f = chebfun(@(x) expm1(x).*(x+1)/2, [aa, bb]);
|
||
|
|
end
|
||
|
|
p = minimax(f, 3);
|
||
|
|
c = poly(p);
|
||
|
|
c = c(:).';
|
||
|
|
if numel(c) < 4
|
||
|
|
c = [zeros(1, 4-numel(c)), c];
|
||
|
|
end
|
||
|
|
fprintf(out, '%.20e %.20e %.20e %.20e\\n', c(4), c(3), c(2), c(1));
|
||
|
|
catch
|
||
|
|
fprintf(out, 'FAIL\\n');
|
||
|
|
end
|
||
|
|
end
|
||
|
|
fclose(fid);
|
||
|
|
fclose(out);
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
subprocess.run(
|
||
|
|
[MATLAB, "-batch", f"run('{program}')"],
|
||
|
|
check=True,
|
||
|
|
timeout=7200,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def load_coeff_file(path: Path) -> list[list[str] | None]:
|
||
|
|
rows: list[list[str] | None] = []
|
||
|
|
for line in path.read_text().splitlines():
|
||
|
|
line = line.strip()
|
||
|
|
if not line or "FAIL" in line:
|
||
|
|
rows.append(None)
|
||
|
|
continue
|
||
|
|
parts = NUMBER.findall(line.replace("D", "E"))
|
||
|
|
if len(parts) < 4:
|
||
|
|
rows.append(None)
|
||
|
|
continue
|
||
|
|
rows.append(parts[:4])
|
||
|
|
return rows
|
||
|
|
|
||
|
|
|
||
|
|
def tournament(
|
||
|
|
sollya: Sollya,
|
||
|
|
pieces_by_key: dict[tuple[str, int], list[Piece]],
|
||
|
|
others: dict[str, list[list[str] | None]],
|
||
|
|
) -> dict[str, int]:
|
||
|
|
wins = {"sollya": 0, "mathematica": 0, "maple": 0, "matlab": 0}
|
||
|
|
index = 0
|
||
|
|
for key in pieces_by_key:
|
||
|
|
name, k = key
|
||
|
|
q = q_of(k)
|
||
|
|
for piece in pieces_by_key[key]:
|
||
|
|
best_err = piece.err
|
||
|
|
best_coeffs = piece.coeffs
|
||
|
|
best_source = "sollya"
|
||
|
|
for source in ("mathematica", "maple", "matlab"):
|
||
|
|
row = others[source][index] if index < len(others[source]) else None
|
||
|
|
if row is None:
|
||
|
|
continue
|
||
|
|
try:
|
||
|
|
coeffs = quantize(row, q)
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
err = sollya.score(name, piece.left, piece.right, k, q, coeffs)
|
||
|
|
if err is not None and err < best_err:
|
||
|
|
best_err = err
|
||
|
|
best_coeffs = coeffs
|
||
|
|
best_source = source
|
||
|
|
piece.coeffs = best_coeffs
|
||
|
|
piece.err = best_err
|
||
|
|
piece.source = best_source
|
||
|
|
wins[best_source] += 1
|
||
|
|
index += 1
|
||
|
|
print(f"tournament {name} k={k} done", flush=True)
|
||
|
|
return wins
|
||
|
|
|
||
|
|
|
||
|
|
def split_i128(value: int) -> tuple[int, int]:
|
||
|
|
raw = value + (1 << 128) if value < 0 else value
|
||
|
|
lo = raw & ((1 << 64) - 1)
|
||
|
|
hi = (raw >> 64) & ((1 << 64) - 1)
|
||
|
|
if hi >= (1 << 63):
|
||
|
|
hi -= 1 << 64
|
||
|
|
return hi, lo
|
||
|
|
|
||
|
|
|
||
|
|
TAGS = {"lecun": "LECUN", "hard": "HARDELISH"}
|
||
|
|
|
||
|
|
|
||
|
|
def emit_table(name: str, k: int, pieces: list[Piece]) -> str:
|
||
|
|
knots = [pieces[0].left] + [piece.right for piece in pieces]
|
||
|
|
q = q_of(k)
|
||
|
|
tag = TAGS[name]
|
||
|
|
lines = [f"static constexpr std::int64_t {tag}_K{k}_KNOTS[] = {{"]
|
||
|
|
lines.append(" " + ", ".join(str(knot) for knot in knots) + ",")
|
||
|
|
lines.append("};")
|
||
|
|
lines.append(f"static constexpr grotto::principal_detail::cubic_bits {tag}_K{k}_PIECES[] = {{")
|
||
|
|
for piece in pieces:
|
||
|
|
his, los = [], []
|
||
|
|
for coeff in piece.coeffs:
|
||
|
|
hi, lo = split_i128(coeff)
|
||
|
|
his.append(str(hi))
|
||
|
|
los.append(f"{lo}u")
|
||
|
|
lines.append(" {{" + ", ".join(his) + "}, {" + ", ".join(los) + "}},")
|
||
|
|
lines.append("};")
|
||
|
|
lines.append(
|
||
|
|
f"static constexpr window_table {tag}_K{k}_TABLE"
|
||
|
|
f"{{{tag}_K{k}_KNOTS, {tag}_K{k}_PIECES, {len(pieces)}, {q}}};"
|
||
|
|
)
|
||
|
|
lines.append("")
|
||
|
|
return "\n".join(lines)
|
||
|
|
|
||
|
|
|
||
|
|
def emit(pieces_by_key: dict[tuple[str, int], list[Piece]], wins: dict[str, int], path: Path) -> None:
|
||
|
|
chunks = [
|
||
|
|
"// Generated on mocha2.",
|
||
|
|
"// Sollya fpminimax chose the knots (absolute error at most half an ulp).",
|
||
|
|
"// Each cubic is the best of Sollya, Mathematica Remez, Maple numapprox[minimax],",
|
||
|
|
"// and MATLAB/Chebfun minimax after rounding coefficients to 2^{-(k+16)}.",
|
||
|
|
"// Wins: " + ", ".join(f"{name} {count}" for name, count in wins.items()) + ".",
|
||
|
|
"// lecun is the positive half of an odd map; hard covers (-1, 0).",
|
||
|
|
"// Do not edit.",
|
||
|
|
"#pragma once",
|
||
|
|
"",
|
||
|
|
]
|
||
|
|
ks = [k for (name, k) in pieces_by_key if name == "lecun"]
|
||
|
|
for name in ("lecun", "hard"):
|
||
|
|
for k in ks:
|
||
|
|
chunks.append(emit_table(name, k, pieces_by_key[(name, k)]))
|
||
|
|
tag = TAGS[name]
|
||
|
|
refs = ",\n ".join(f"&{tag}_K{k}_TABLE" for k in ks)
|
||
|
|
chunks.append(
|
||
|
|
f"static constexpr window_table const* const {tag}[{len(ks)}] = {{\n {refs},\n}};"
|
||
|
|
)
|
||
|
|
chunks.append("")
|
||
|
|
tails = ", ".join(str(lecun_tail(k)) for k in ks)
|
||
|
|
chunks.append(f"static constexpr std::int64_t LECUN_TAIL[{len(ks)}] = {{\n {tails},\n}};")
|
||
|
|
chunks.append("")
|
||
|
|
path.write_text("\n".join(chunks))
|
||
|
|
|
||
|
|
|
||
|
|
def summarize(pieces_by_key: dict[tuple[str, int], list[Piece]], wins: dict[str, int]) -> None:
|
||
|
|
summary = {"wins": wins, "functions": {}}
|
||
|
|
for name in ("lecun", "hard"):
|
||
|
|
summary["functions"][name] = {}
|
||
|
|
for k in selected_precisions():
|
||
|
|
pieces = pieces_by_key[(name, k)]
|
||
|
|
summary["functions"][name][str(k)] = {
|
||
|
|
"parts": len(pieces),
|
||
|
|
"max_abs_error": max(piece.err for piece in pieces),
|
||
|
|
"half_ulp": half_ulp(k),
|
||
|
|
}
|
||
|
|
(WORK / "summary.json").write_text(json.dumps(summary, indent=2))
|
||
|
|
print(json.dumps(summary, indent=2), flush=True)
|
||
|
|
|
||
|
|
|
||
|
|
def self_test() -> None:
|
||
|
|
sollya = Sollya()
|
||
|
|
got = sollya.fit("lecun", 0, 1 << 15, 16, q_of(16), half_ulp(16))
|
||
|
|
print("sollya", got[0] if got else None)
|
||
|
|
sollya.close()
|
||
|
|
|
||
|
|
|
||
|
|
def selected_precisions() -> tuple[int, ...]:
|
||
|
|
if "--k" in sys.argv:
|
||
|
|
return (int(sys.argv[sys.argv.index("--k") + 1]),)
|
||
|
|
return PREC
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
if len(sys.argv) > 1 and sys.argv[1] == "--self-test":
|
||
|
|
self_test()
|
||
|
|
return
|
||
|
|
WORK.mkdir(parents=True, exist_ok=True)
|
||
|
|
sollya = Sollya()
|
||
|
|
pieces_by_key: dict[tuple[str, int], list[Piece]] = {}
|
||
|
|
for k in selected_precisions():
|
||
|
|
end = lecun_end(k)
|
||
|
|
print(f"march lecun k={k} end={end} tail={lecun_tail(k)}", flush=True)
|
||
|
|
pieces_by_key[("lecun", k)] = march(sollya, "lecun", 0, end, k)
|
||
|
|
one = 1 << k
|
||
|
|
print(f"march hard k={k}", flush=True)
|
||
|
|
pieces_by_key[("hard", k)] = march(sollya, "hard", -one, 0, k)
|
||
|
|
interval_path = WORK / "intervals.txt"
|
||
|
|
write_intervals(pieces_by_key, interval_path)
|
||
|
|
print("intervals", sum(len(v) for v in pieces_by_key.values()), flush=True)
|
||
|
|
jobs = {
|
||
|
|
"maple": (run_maple, WORK / "maple_coeffs.txt"),
|
||
|
|
"mathematica": (run_math, WORK / "mma_coeffs.txt"),
|
||
|
|
"matlab": (run_matlab, WORK / "matlab_coeffs.txt"),
|
||
|
|
}
|
||
|
|
for name, (fn, out) in jobs.items():
|
||
|
|
# Run sequentially inside this process: MATLAB and Maple both want a license,
|
||
|
|
# and a failure is easier to see one at a time. They do not share state.
|
||
|
|
print("running", name, flush=True)
|
||
|
|
if out.exists():
|
||
|
|
out.unlink()
|
||
|
|
fn(interval_path, out)
|
||
|
|
print("finished", name, out.stat().st_size, flush=True)
|
||
|
|
expected = sum(len(v) for v in pieces_by_key.values())
|
||
|
|
others = {
|
||
|
|
"maple": load_coeff_file(WORK / "maple_coeffs.txt"),
|
||
|
|
"mathematica": load_coeff_file(WORK / "mma_coeffs.txt"),
|
||
|
|
"matlab": load_coeff_file(WORK / "matlab_coeffs.txt"),
|
||
|
|
}
|
||
|
|
for source, rows in others.items():
|
||
|
|
if len(rows) != expected:
|
||
|
|
raise SystemExit(f"{source} wrote {len(rows)} rows, expected {expected}")
|
||
|
|
wins = tournament(sollya, pieces_by_key, others)
|
||
|
|
sollya.close()
|
||
|
|
emit(pieces_by_key, wins, WORK / "window_extra_tables.inc")
|
||
|
|
summarize(pieces_by_key, wins)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|