Files
wyj 0a46a7095b Feat: Complete adaptive geodesic tracing with DP54
Add error-controlled DP5(4) integration and trusted first-crossing localization, including non-monotonic energy thresholds and representable-time stepping.

Preserve adaptive state and independent step/time retry grants across RayPool, refinement and movie scheduling. Expose numerical controls, record actual persistent-sample costs, and add v3 lens-map provenance with legacy v2 RK4 import.

Use DP54 by default and select an 8M Schwarzschild maximum step from bounded scans and a two-run 4K comparison. Retain the conservative minimum-step guard and document critical-ray and backend capability limits. Archive self-contained benchmark inputs and raw output; keep fixed RK4 HDR references explicit.

Validation: make -B -j4 BUILD_TYPE=Debug test passed; explicit RK4 HDR references have zero differences. Bounded convergence checks, benchmark reproduction, Release build and focused reviews passed. No numerical-relativity backend is added.
2026-10-05 20:27:42 -04:00

125 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""One-task, at-most-two-attempt 4K comparison (R100, 3840x2160, FOV45).
This driver does NOT run as part of routine verification. It refuses to do
anything unless ``--authorize-two-4k`` is passed explicitly, and an attempt
ledger makes the task non-repeatable. Any future execution requires a fresh,
explicit user authorization for that task; authorization never extends to later
tasks or routine tests.
Usage:
run_4k_pair.py --authorize-two-4k [--out DIR]
OUT defaults to <repo>/local/adaptive_bounds_4k (distinct from any older
one-off ledger). The renderer binary is expected at
build/Release/schwarzschild_sky (see README for the make command).
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
from mesh_maps import load, compare
ROOT = Path(__file__).resolve().parents[2]
ENV = dict(os.environ, OMP_NUM_THREADS='16', OMP_DYNAMIC='FALSE')
BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend'
REFUSAL = (
'REFUSING TO RUN: the 4K hmax 2-vs-8 pair is a one-task, at-most-two-attempt '
'experiment. Pass --authorize-two-4k only after an explicit fresh user '
'authorization for this task. It is not a routine verification target.')
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--authorize-two-4k', action='store_true',
help='explicit authorization for the two-attempt 4K pair')
ap.add_argument('--out', default=str(ROOT / 'local/adaptive_bounds_4k'),
help='output directory (default: local/adaptive_bounds_4k)')
args = ap.parse_args()
if not args.authorize_two_4k:
print(REFUSAL, file=sys.stderr)
return 2
out = Path(args.out).resolve()
binary = ROOT / 'build/Release/schwarzschild_sky'
if not binary.exists():
print(f'missing {binary}; build it first:\n {BUILD_CMD}',
file=sys.stderr)
return 2
out.mkdir(parents=True, exist_ok=True)
ledger = out / 'attempts.json'
attempts = json.loads(ledger.read_text()) if ledger.exists() else []
if attempts:
print('This one-task pair has already been attempted; no reruns '
'authorized.', file=sys.stderr)
return 3
catalog = out / 'single_dim_star.csv'
catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n'
'262.5,-30,6000,1e-30\n')
with (out / 'cpu.txt').open('w') as f:
subprocess.run(['lscpu'], stdout=f, check=True)
metadata = {
'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
'utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'source_sha256': {
p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
for p in ('src/main.c', 'src/geodesic.c', 'src/frame.c')},
'catalog': 'one negligible synthetic point; no survey catalog',
'scope': 'README R100 45-degree 3840x2160 coarse16 refine4 jacobian0.2',
'threads': 16, 'max_attempts': 2,
'authorization': 'explicit --authorize-two-4k for this task only'}
(out / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n')
maps = {}
results = {}
for upper in (2, 8):
name = f'r100_4k_hmax{upper}'
cmd = [str(binary), '--integrator', 'dp54', '--width', '3840',
'--height', '2160', '--look-ra-deg', '262.5',
'--look-dec-deg', '-30', '--fov-deg', '45',
'--observer-radius', '100', '--coarse-cell-pixels', '16',
'--refine-max-level', '4', '--refine-jacobian-min', '.2',
'--catalog', str(catalog), '--exposure', '1',
'--tone-map', 'reinhard', '--psf-min-y', '1e-20',
'--psf-relative-tail', '1e-4', '--ode-min-step', '1e-12',
'--ode-max-step', str(upper), '--verbose',
'--lens-map-output', str(out / (name + '.grlens')),
'--output', str(out / (name + '.png'))]
attempts.append({'upper': upper, 'command': shlex.join(cmd),
'state': 'attempted'})
ledger.write_text(json.dumps(attempts, indent=2) + '\n')
start = time.perf_counter()
with (out / (name + '.log')).open('w') as log:
log.write('OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE ' +
shlex.join(cmd) + '\n')
log.flush()
result = subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log,
stderr=subprocess.STDOUT, timeout=900)
attempts[-1].update(returncode=result.returncode,
wall_seconds=time.perf_counter() - start,
state='completed')
ledger.write_text(json.dumps(attempts, indent=2) + '\n')
if result.returncode:
raise SystemExit(f'4K attempt failed: {name}; no automatic retry.')
info, maps[upper] = load(out / (name + '.grlens'))
info['wall_seconds'] = attempts[-1]['wall_seconds']
results[name] = info
print(name, json.dumps(info), flush=True)
results['comparison'] = compare(maps[2], maps[8])
p, q = (results[f'r100_4k_hmax{h}']['provenance'] for h in (2, 8))
assert p['max_step'] == 2 and q['max_step'] == 8
assert {k: v for k, v in p.items() if k != 'max_step'} == \
{k: v for k, v in q.items() if k != 'max_step'}
print('comparison', json.dumps(results['comparison']), flush=True)
(out / 'summary.json').write_text(json.dumps(results, indent=2) + '\n')
return 0
if __name__ == '__main__':
sys.exit(main())