675 lines
35 KiB
Python
675 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the production DP54 default, v3 lens-map provenance/replay and the
|
|
adaptive retry/budget policy from the CLI.
|
|
|
|
The production default is now adaptive Dormand-Prince 5(4). These checks
|
|
require that a run without --integrator exports wire code 1 and is physically
|
|
and bit-for-bit equal to an explicit --integrator dp54 run, so `make test`
|
|
truly covers the production default rather than only the explicit path.
|
|
--integrator rk4 stays available for the legacy wire code and for a
|
|
fixed-step reference convergence check. Small CPU 16x8/32x16 scenes keep the
|
|
runtime short; physical comparisons use stored lens-map endpoints, not only the
|
|
rendered PNG.
|
|
"""
|
|
import math
|
|
import os
|
|
import re
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
# Keep scratch data inside the pre-approved OpenCode scratch directory instead
|
|
# of creating directories directly under the system temporary root.
|
|
TMP_ROOT = Path('/tmp/opencode')
|
|
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
|
|
|
BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve()
|
|
TESTDIR = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else BUILD
|
|
ENV = dict(os.environ, OMP_NUM_THREADS='4')
|
|
|
|
# v3 wire offsets (see src/lens_map.c). The header is deliberately not part of
|
|
# the payload CRC.
|
|
VERSION_OFFSET = 8
|
|
FRAME_COUNT_OFFSET = 32
|
|
PROVENANCE_OFFSET = 40
|
|
PROVENANCE_V3_OFFSET = 100
|
|
VERTEX_COUNT_OFFSET = 200
|
|
TRIANGLE_COUNT_OFFSET = 208
|
|
VERTEX_START = 224
|
|
VERTEX_SIZE = 108
|
|
TRIANGLE_SIZE = 32
|
|
|
|
ATOL_FIELDS = ('atol_x', 'atol_Pi', 'atol_L', 'rtol', 'min_step', 'max_step',
|
|
'max_lookback_time', 'retry_lookback_increment',
|
|
'max_total_lookback_time')
|
|
# Machine-roundoff floor for comparing two adaptive integrations; below this a
|
|
# difference carries no convergence information.
|
|
ROUNDOFF_FLOOR = 1e-12
|
|
ENDPOINT_ASSERT = 1e-6
|
|
|
|
|
|
def run(binary, *args, ok=True, env=ENV):
|
|
result = subprocess.run([str(binary), *map(str, args)], env=env,
|
|
capture_output=True, text=True)
|
|
if (result.returncode == 0) != ok:
|
|
raise AssertionError(
|
|
f'{binary.name} {args}: rc={result.returncode}\n{result.stderr}')
|
|
return result
|
|
|
|
|
|
def image_payload(path):
|
|
data = path.read_bytes()
|
|
assert data[:8] == b'\x89PNG\r\n\x1a\n', f'not a PNG: {path}'
|
|
offset, compressed = 8, bytearray()
|
|
while offset < len(data):
|
|
count, kind = struct.unpack_from('>I4s', data, offset)
|
|
payload = data[offset + 8:offset + 8 + count]
|
|
if kind == b'IDAT':
|
|
compressed.extend(payload)
|
|
offset += count + 12
|
|
return bytes(zlib.decompress(compressed))
|
|
|
|
|
|
def map_provenance(path):
|
|
data = path.read_bytes()
|
|
assert data[:8] == b'GRLENS\x01\x00'
|
|
version = struct.unpack_from('<I', data, VERSION_OFFSET)[0]
|
|
frame_count = struct.unpack_from('<Q', data, FRAME_COUNT_OFFSET)[0]
|
|
base = PROVENANCE_OFFSET
|
|
prov = {}
|
|
prov['threshold_kind'], prov['threshold_policy_version'] = struct.unpack_from(
|
|
'<II', data, base)
|
|
prov['threshold_value'] = struct.unpack_from('<d', data, base + 8)[0]
|
|
(prov['retry_step_increment'], prov['max_total_steps'], prov['max_level'],
|
|
prov['integrator']) = struct.unpack_from('<IIII', data, base + 16)
|
|
prov['min_edge_pixels'] = struct.unpack_from('<d', data, base + 32)[0]
|
|
prov['min_area_pixels2'] = struct.unpack_from('<d', data, base + 40)[0]
|
|
prov['coordinate_time_step'] = struct.unpack_from('<d', data, base + 48)[0]
|
|
prov['initial_max_steps'] = struct.unpack_from('<I', data, base + 56)[0]
|
|
if version == 3:
|
|
off = PROVENANCE_V3_OFFSET
|
|
for name in ATOL_FIELDS:
|
|
prov[name] = struct.unpack_from('<d', data, off)[0]
|
|
off += 8
|
|
prov['max_consecutive_rejections'] = struct.unpack_from('<I', data, off)[0]
|
|
return version, frame_count, prov
|
|
|
|
|
|
def map_vertices(path):
|
|
data = path.read_bytes()
|
|
version = struct.unpack_from('<I', data, VERSION_OFFSET)[0]
|
|
assert version == 3, f'expected a v3 map, got v{version}'
|
|
vertices = struct.unpack_from('<Q', data, VERTEX_COUNT_OFFSET)[0]
|
|
triangles = struct.unpack_from('<Q', data, TRIANGLE_COUNT_OFFSET)[0]
|
|
offset = VERTEX_START
|
|
values = []
|
|
for _ in range(vertices):
|
|
values.append(struct.unpack_from('<9dIIIQQQ', data, offset))
|
|
offset += VERTEX_SIZE
|
|
return values, data[offset:offset + triangles * TRIANGLE_SIZE]
|
|
|
|
|
|
def endpoint_deviation(a, b):
|
|
"""(mismatched provenance count, max direction/log-g deviation).
|
|
|
|
Vertices whose end_id/outcome/reason differ are counted as mismatches and
|
|
excluded from the numeric deviation; the caller requires zero mismatches.
|
|
"""
|
|
mismatches = 0
|
|
worst = 0.0
|
|
assert len(a) == len(b)
|
|
for x, y in zip(a, b):
|
|
if x[9:12] != y[9:12]:
|
|
mismatches += 1
|
|
continue
|
|
for k in (3, 4, 5, 6, 7, 8):
|
|
worst = max(worst, abs(x[k] - y[k]))
|
|
return mismatches, worst
|
|
|
|
|
|
def trace_cost(text, label):
|
|
match = re.search(label + r' trace cost: accepted=(\d+) rejected=(\d+) '
|
|
r'rhs=(\d+)', text)
|
|
return None if match is None else match.groups()
|
|
|
|
|
|
def sum_rhs(vertices):
|
|
return sum(v[14] for v in vertices)
|
|
|
|
|
|
def alcubierre_budget(escape, vs, dark_threshold):
|
|
"""Reference implementation of the production Alcubierre time allowance:
|
|
B = 5*escape / max(|1-|v_s||, exp(-D)), with the DBL_MIN..DBL_MAX/4
|
|
saturation and the log-space fallback used when the ordinary division is
|
|
not finite and positive. exp(D) is never formed."""
|
|
sep = abs(1.0 - abs(vs))
|
|
floor = math.exp(-dark_threshold)
|
|
denom = max(sep, floor)
|
|
upper = sys.float_info.max / 4.0
|
|
if denom > 0.0 and math.isfinite(denom):
|
|
scaled = 5.0 * (escape / denom)
|
|
if math.isfinite(scaled) and scaled > 0.0:
|
|
return min(max(scaled, sys.float_info.min), upper)
|
|
log_sep = math.log(sep) if sep > 0.0 else -math.inf
|
|
log_budget = math.log(5.0) + math.log(escape) - max(log_sep, -dark_threshold)
|
|
if not math.isfinite(log_budget):
|
|
log_budget = math.log(upper)
|
|
log_budget = min(log_budget, math.log(upper))
|
|
log_budget = max(log_budget, math.log(sys.float_info.min))
|
|
return min(max(math.exp(log_budget), sys.float_info.min), upper)
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
|
|
dir=str(TMP_ROOT)) as directory:
|
|
tmp = Path(directory)
|
|
for backend in ('minkowski', 'schwarzschild'):
|
|
binary = BUILD / f'{backend}_sky'
|
|
if not binary.exists():
|
|
print(f'{backend}: binary absent, skipping', flush=True)
|
|
continue
|
|
help_text = run(binary, '--help').stdout
|
|
for option in ('--integrator', '--ode-rtol', '--ode-atol-x',
|
|
'--ode-atol-pi', '--ode-atol-l', '--ode-initial-step',
|
|
'--ode-min-step', '--ode-max-step',
|
|
'--ode-max-rejections', '--trace-max-steps',
|
|
'--trace-lookback-time', '--retry-step-increment',
|
|
'--max-total-steps', '--retry-lookback-increment',
|
|
'--max-total-lookback-time'):
|
|
assert option in help_text, (backend, option)
|
|
ext = 'png' if '.png' in help_text else 'ppm'
|
|
hdr_available = '--hdr-output' in help_text
|
|
hdr_args = ['--hdr-output'] if hdr_available else []
|
|
common = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', 32,
|
|
'--height', 16, '--fov-deg', 80, '--exposure', 1e-3,
|
|
'--coarse-cell-pixels', 8, '--refine-max-level', 0,
|
|
'--psf-relative-tail', 1e-4]
|
|
|
|
def single(name, *options, ok=True, env=ENV, use_common=common):
|
|
out = tmp / f'{backend}_{name}.{ext}'
|
|
result = run(binary, *use_common, '--output', out, *options,
|
|
ok=ok, env=env)
|
|
return out, result
|
|
|
|
# 1) The production default (no --integrator) must be DP54 and must
|
|
# match an explicit --integrator dp54 run physically and in its
|
|
# PNG/HDR output.
|
|
dflt_map = tmp / f'{backend}_dflt.grlens'
|
|
dflt_out, dflt_run = single('dflt', *hdr_args, '--verbose',
|
|
'--lens-map-output', dflt_map)
|
|
version, frame_count, dflt_prov = map_provenance(dflt_map)
|
|
assert version == 3 and frame_count == 1
|
|
assert dflt_prov['integrator'] == 1, dflt_prov
|
|
assert dflt_prov['min_step'] == 1e-12, dflt_prov
|
|
assert dflt_prov['max_step'] == {'minkowski': 16.0,
|
|
'schwarzschild': 8.0}[backend], dflt_prov
|
|
assert dflt_prov['min_step'] <= dflt_prov['coordinate_time_step'] \
|
|
<= dflt_prov['max_step']
|
|
assert dflt_prov['atol_x'] > 0 and dflt_prov['rtol'] > 0
|
|
assert dflt_prov['max_lookback_time'] > 0
|
|
assert dflt_prov['max_consecutive_rejections'] > 0
|
|
expl_map = tmp / f'{backend}_expl.grlens'
|
|
expl_out, _ = single('expl', *hdr_args, '--integrator', 'dp54',
|
|
'--lens-map-output', expl_map)
|
|
assert dflt_map.read_bytes() == expl_map.read_bytes(), \
|
|
'default map differs from explicit dp54'
|
|
assert image_payload(dflt_out) == image_payload(expl_out)
|
|
if hdr_available:
|
|
dflt_hdr = dflt_out.with_name(dflt_out.stem + '_HDR.fits')
|
|
expl_hdr = expl_out.with_name(expl_out.stem + '_HDR.fits')
|
|
assert dflt_hdr.read_bytes() == expl_hdr.read_bytes()
|
|
dflt_vertices, _ = map_vertices(dflt_map)
|
|
|
|
# 2) The legacy RK4 wire code must be 0 and its cost counters must be
|
|
# real (nonzero RHS evaluations), not legacy zeros.
|
|
rk4_map = tmp / f'{backend}_rk4.grlens'
|
|
single('rk4', '--integrator', 'rk4', '--lens-map-output', rk4_map)
|
|
_, _, rk4_prov = map_provenance(rk4_map)
|
|
assert rk4_prov['integrator'] == 0, rk4_prov
|
|
rk4_vertices, _ = map_vertices(rk4_map)
|
|
assert sum_rhs(rk4_vertices) > 0, 'RK4 RHS cost counters are not real'
|
|
|
|
# 3) Same-camera tolerance convergence with three levels. Outcome,
|
|
# reason and end must not change between levels, and the deviation
|
|
# from the tightest reference must shrink as the tolerance tightens.
|
|
# These are local ODE tolerances, not a global sky-error bound.
|
|
tol_maps = {}
|
|
for tol in ('1e-7', '1e-9', '1e-12'):
|
|
m = tmp / f'{backend}_tol_{tol}.grlens'
|
|
single(f'tol_{tol}', '--integrator', 'dp54', '--ode-rtol', tol,
|
|
'--ode-atol-x', tol, '--ode-atol-pi', tol,
|
|
'--ode-atol-l', tol, '--lens-map-output', m)
|
|
tol_maps[tol] = map_vertices(m)[0]
|
|
mism7, err7 = endpoint_deviation(tol_maps['1e-7'], tol_maps['1e-12'])
|
|
mism9, err9 = endpoint_deviation(tol_maps['1e-9'], tol_maps['1e-12'])
|
|
assert mism7 == 0 and mism9 == 0, \
|
|
f'{backend}: tolerance levels disagree on outcome/end'
|
|
assert err9 < ENDPOINT_ASSERT, (backend, 'default vs tight', err9)
|
|
assert err7 + ROUNDOFF_FLOOR >= err9, \
|
|
f'{backend}: tightening tolerance did not reduce error ' \
|
|
f'({err7} -> {err9})'
|
|
print(f'{backend}: default==dp54, tol errors 1e-7={err7:.3g} '
|
|
f'1e-9={err9:.3g}', flush=True)
|
|
|
|
# 4) Render-only replay of the default map must be bit-identical and
|
|
# its stored statistics must equal the live trace cost.
|
|
replay_out = tmp / f'{backend}_replay.{ext}'
|
|
replay_run = run(binary, *common, *hdr_args, '--lens-map-input',
|
|
dflt_map, '--verbose', '--output', replay_out)
|
|
assert image_payload(replay_out) == image_payload(dflt_out)
|
|
if hdr_available:
|
|
replay_hdr = replay_out.with_name(replay_out.stem + '_HDR.fits')
|
|
assert dflt_out.with_name(dflt_out.stem + '_HDR.fits').read_bytes() \
|
|
== replay_hdr.read_bytes()
|
|
live_cost = trace_cost(dflt_run.stdout, 'Frame 0')
|
|
replay_cost = trace_cost(replay_run.stdout, 'Imported map')
|
|
assert live_cost is not None and replay_cost is not None
|
|
assert live_cost == replay_cost, (live_cost, replay_cost)
|
|
|
|
# 5) Explicit zeros in the retry policy must survive, not be filled in
|
|
# by the derived defaults.
|
|
zero_step_map = tmp / f'{backend}_zero_step.grlens'
|
|
single('zero_step', '--integrator', 'dp54', '--retry-step-increment',
|
|
'0', '--lens-map-output', zero_step_map)
|
|
_, _, zero_step = map_provenance(zero_step_map)
|
|
assert zero_step['retry_step_increment'] == 0, zero_step
|
|
assert zero_step['max_total_steps'] > 0, zero_step
|
|
zero_time_map = tmp / f'{backend}_zero_time.grlens'
|
|
single('zero_time', '--integrator', 'dp54',
|
|
'--retry-lookback-increment', '0', '--lens-map-output',
|
|
zero_time_map)
|
|
_, _, zero_time = map_provenance(zero_time_map)
|
|
assert zero_time['retry_lookback_increment'] == 0, zero_time
|
|
assert zero_time['max_total_lookback_time'] >= \
|
|
zero_time['max_lookback_time'], zero_time
|
|
|
|
# 6) RK4 rejects every DP-only option rather than silently ignoring it.
|
|
rk4_errors = [
|
|
(['--integrator', 'rk4', '--ode-rtol', 1e-9], 'applies only'),
|
|
(['--integrator', 'rk4', '--ode-min-step', 1e-9], 'applies only'),
|
|
(['--integrator', 'rk4', '--ode-max-step', 1e-3], 'applies only'),
|
|
(['--integrator', 'rk4', '--ode-max-rejections', 4], 'applies only'),
|
|
(['--integrator', 'rk4', '--trace-lookback-time', 1], 'applies only'),
|
|
(['--integrator', 'rk4', '--retry-lookback-increment', 1], 'applies only'),
|
|
(['--integrator', 'rk4', '--max-total-lookback-time', 2], 'applies only'),
|
|
]
|
|
# DP cross-field validation and malformed values fail immediately.
|
|
invalid = [
|
|
(['--integrator', 'bogus'], None),
|
|
(['--integrator'], None),
|
|
(['--integrator', 'dp54', '--ode-rtol', 0], None),
|
|
(['--integrator', 'dp54', '--ode-rtol', -1], None),
|
|
(['--integrator', 'dp54', '--ode-atol-x', 'nan'], None),
|
|
(['--integrator', 'dp54', '--ode-max-rejections', 0], None),
|
|
(['--integrator', 'dp54', '--trace-max-steps', 0], None),
|
|
(['--integrator', 'dp54', '--ode-initial-step', 5,
|
|
'--ode-max-step', 1], 'min <= initial <= max'),
|
|
(['--integrator', 'dp54', '--ode-min-step', 4,
|
|
'--ode-initial-step', 2], 'min <= initial <= max'),
|
|
(['--integrator', 'dp54', '--trace-lookback-time', 0], None),
|
|
(['--integrator', 'dp54', '--retry-step-increment', -1], None),
|
|
]
|
|
for options, message in rk4_errors + invalid:
|
|
missing = tmp / 'adaptive_should_not_exist.csv'
|
|
result = run(binary, '--catalog', missing, *options, ok=False)
|
|
if message:
|
|
assert message in result.stderr, (options, result.stderr)
|
|
assert not missing.exists(), result.stderr
|
|
|
|
# 7) Replay must consume the stored policy: explicit tracing options
|
|
# cannot be layered on top of --lens-map-input.
|
|
conflict = run(binary, *common, '--lens-map-input', dflt_map,
|
|
'--integrator', 'rk4', '--output',
|
|
tmp / 'conflict.png', ok=False)
|
|
assert 'cannot be combined with --lens-map-input' in conflict.stderr, \
|
|
conflict.stderr
|
|
conflict2 = run(binary, *common, '--lens-map-input', dflt_map,
|
|
'--trace-max-steps', 100, '--output',
|
|
tmp / 'conflict2.png', ok=False)
|
|
assert 'cannot be combined with --lens-map-input' in conflict2.stderr, \
|
|
conflict2.stderr
|
|
conflict3 = run(binary, *common, '--lens-map-input', dflt_map,
|
|
'--integrator', 'dp54', '--ode-rtol', 1e-9,
|
|
'--output', tmp / 'conflict3.png', ok=False)
|
|
assert 'cannot be combined with --lens-map-input' in conflict3.stderr, \
|
|
conflict3.stderr
|
|
|
|
# 8) Single vs movie: the same physical observer event must agree, and
|
|
# threads/slabs must not change the stored adaptive endpoints.
|
|
track = tmp / f'{backend}.csv'
|
|
observer_test = TESTDIR / f'test_observer_{backend}'
|
|
if observer_test.exists():
|
|
run(observer_test, track)
|
|
moving_single = tmp / f'{backend}_moving.grlens'
|
|
single('moving', '--observer-position', 3, -4, 5,
|
|
'--observer-velocity', 0.2, -0.1, 0.3,
|
|
'--look-ra-deg', 37, '--look-dec-deg', -23,
|
|
'--camera-roll-deg', 19, '--lens-map-output', moving_single)
|
|
movie_map = tmp / f'{backend}_movie.grlens'
|
|
run(binary, *common, '--observer-track', track, '--frames-dir', tmp,
|
|
'--frames-prefix', f'{backend}_movie', '--duration', 0,
|
|
'--fps', 1, '--lens-map-output', movie_map)
|
|
single_v, _ = map_vertices(moving_single)
|
|
movie_v, _ = map_vertices(movie_map)
|
|
mism, dev = endpoint_deviation(single_v, movie_v)
|
|
assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev)
|
|
|
|
thread_maps = []
|
|
for threads in (1, 2, 4):
|
|
m = tmp / f'{backend}_movie_{threads}thr.grlens'
|
|
run(binary, *common, '--observer-track', track, '--frames-dir',
|
|
tmp, '--frames-prefix', f'{backend}_m{threads}',
|
|
'--duration', 0, '--fps', 1, '--lens-map-output', m,
|
|
env=dict(ENV, OMP_NUM_THREADS=str(threads)))
|
|
thread_maps.append(m)
|
|
reference = thread_maps[0].read_bytes()
|
|
for m in thread_maps[1:]:
|
|
assert m.read_bytes() == reference, \
|
|
f'{backend}: DP movie map changed across threads'
|
|
|
|
slab_maps = []
|
|
for slab in (2, 8, 64):
|
|
m = tmp / f'{backend}_slab_{slab}.grlens'
|
|
run(binary, *common, '--observer-track', track, '--frames-dir',
|
|
tmp, '--frames-prefix', f'{backend}_s{slab}',
|
|
'--slab-duration', slab, '--duration', 0, '--fps', 1,
|
|
'--lens-map-output', m)
|
|
slab_maps.append(m)
|
|
base_v, _ = map_vertices(slab_maps[0])
|
|
for m in slab_maps[1:]:
|
|
other_v, _ = map_vertices(m)
|
|
mism, dev = endpoint_deviation(base_v, other_v)
|
|
assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev)
|
|
print(f'{backend}: DP movie threads/slabs agree', flush=True)
|
|
|
|
# 9) Budget: a tiny initial coordinate-time budget leaves UNRESOLVED
|
|
# rays; with no room to grow the publication gate refuses the frame,
|
|
# while retry increments that can grow resolve it.
|
|
if backend == 'schwarzschild':
|
|
refused = tmp / f'{backend}_refused.{ext}'
|
|
small = ['--integrator', 'dp54', '--trace-lookback-time', 1e-6,
|
|
'--retry-lookback-increment', 0,
|
|
'--max-total-lookback-time', 1e-6]
|
|
result = run(binary, *common, '--output', refused, *small, ok=False)
|
|
assert 'Incomplete render refused' in result.stderr, result.stderr
|
|
assert not refused.exists()
|
|
allow = tmp / f'{backend}_allow.{ext}'
|
|
run(binary, *common, '--output', allow, '--allow-incomplete', *small)
|
|
assert allow.exists()
|
|
|
|
direct = tmp / f'{backend}_direct.{ext}'
|
|
direct_result = subprocess.run(
|
|
[str(binary), *map(str, common), '--output', str(direct),
|
|
'--integrator', 'dp54', '--trace-lookback-time', '4000'],
|
|
env=ENV, capture_output=True, text=True)
|
|
if direct_result.returncode == 0:
|
|
retried = tmp / f'{backend}_retried.{ext}'
|
|
run(binary, *common, '--output', retried, '--integrator', 'dp54',
|
|
'--trace-lookback-time', 1e-6,
|
|
'--retry-lookback-increment', 25,
|
|
'--max-total-lookback-time', 4000)
|
|
assert image_payload(retried)
|
|
print('schwarzschild: retry budget resolves previously '
|
|
'unresolved rays', flush=True)
|
|
else:
|
|
print('schwarzschild: direct budget scene still unresolved; '
|
|
'retry-resolution case skipped', flush=True)
|
|
|
|
# 10) Fixed-step reference convergence on a small scene. The
|
|
# accepted budget is explicit and large so the finer step does
|
|
# not silently shorten the traced history. Both fixed-step
|
|
# levels and the DP54 default must agree below ENDPOINT_ASSERT.
|
|
# If the reference itself does not converge this must FAIL and
|
|
# be reported, never loosened into a false zero.
|
|
ref_common = ['--catalog', 'assets/sky_grid_5deg.csv', '--width',
|
|
16, '--height', 8, '--fov-deg', 80, '--exposure',
|
|
1e-3, '--coarse-cell-pixels', 8, '--refine-max-level',
|
|
0, '--psf-relative-tail', 1e-4]
|
|
|
|
def ref_map(tag, *options):
|
|
m = tmp / f'{backend}_ref_{tag}.grlens'
|
|
run(binary, *ref_common, '--output',
|
|
tmp / f'{backend}_ref_{tag}.{ext}', '--lens-map-output', m,
|
|
*options)
|
|
return map_vertices(m)[0]
|
|
|
|
rk4_04 = ref_map('rk4_04', '--integrator', 'rk4',
|
|
'--ode-initial-step', 0.04, '--trace-max-steps',
|
|
'262144')
|
|
rk4_02 = ref_map('rk4_02', '--integrator', 'rk4',
|
|
'--ode-initial-step', 0.02, '--trace-max-steps',
|
|
'262144')
|
|
dp_tight = ref_map('dp_tight', '--ode-rtol', '1e-12',
|
|
'--ode-atol-x', '1e-12', '--ode-atol-pi',
|
|
'1e-12', '--ode-atol-l', '1e-12')
|
|
assert sum(1 for v in rk4_02 if v[10] == 0) > 0, \
|
|
'reference scene has no escaped ray'
|
|
mism, ref_dev = endpoint_deviation(rk4_04, rk4_02)
|
|
assert mism == 0, 'RK4 reference levels disagree on outcome/end'
|
|
assert ref_dev < ENDPOINT_ASSERT, \
|
|
f'RK4 reference not converged: {ref_dev}; report to parent'
|
|
mism, dp_dev = endpoint_deviation(dp_tight, rk4_02)
|
|
assert mism == 0, 'DP54 default disagrees with RK4 reference'
|
|
assert dp_dev < ENDPOINT_ASSERT, (dp_dev,)
|
|
print(f'schwarzschild: RK4 ref convergence {ref_dev:.3g}, '
|
|
f'DP default vs fine RK4 {dp_dev:.3g}', flush=True)
|
|
|
|
print(f'{backend}: adaptive CLI checks passed', flush=True)
|
|
|
|
# Alcubierre production policy: sub- and super-luminal velocities share one
|
|
# finite resource allowance B = 5*escape / max(|1-|v_s||, exp(-D)); the DP54
|
|
# default must validate, actually integrate the warp feature, and reach both
|
|
# the shared DARK terminal and escapes. All images are tiny 8x4/16x8.
|
|
alc = BUILD / 'alcubierre_sky'
|
|
if not alc.exists():
|
|
print('alcubierre: binary absent, skipping', flush=True)
|
|
else:
|
|
alc_help = run(alc, '--help').stdout
|
|
ext = 'png' if '.png' in alc_help else 'ppm'
|
|
assert '|v_s| < 1' not in alc_help, 'help still claims |v_s| < 1'
|
|
escape1 = 1.0 + 20.0 / 1.0 # R = sigma = 1
|
|
|
|
def alc_camera(vs, ra_deg, dec_deg=0.0):
|
|
return ['--observer-position', '0', '0', '0',
|
|
'--observer-velocity', repr(vs), '0', '0',
|
|
'--look-ra-deg', repr(ra_deg),
|
|
'--look-dec-deg', repr(dec_deg)]
|
|
|
|
def alc_map_prov(tag, vs, D=8.0, radius=1.0, extra=(), allow=True,
|
|
width=8, height=4, cell=4, camera=None,
|
|
catalog=True):
|
|
m = tmp / f'alc_{tag}.grlens'
|
|
args = ['--alcubierre-vs', repr(vs), '--alcubierre-radius',
|
|
repr(radius), '--alcubierre-sigma', '1',
|
|
'--dark-threshold', repr(D), '--width', str(width),
|
|
'--height', str(height), '--fov-deg', 80, '--exposure',
|
|
'1e-3', '--coarse-cell-pixels', str(cell),
|
|
'--refine-max-level', 0, '--psf-relative-tail', 1e-4]
|
|
if catalog:
|
|
args += ['--catalog', 'assets/sky_grid_5deg.csv']
|
|
if camera is not None:
|
|
args += camera
|
|
if allow:
|
|
args.append('--allow-incomplete')
|
|
args += ['--output', str(tmp / f'alc_{tag}.{ext}'),
|
|
'--lens-map-output', str(m), *extra]
|
|
run(alc, *args)
|
|
return map_provenance(m)[2], m
|
|
|
|
# 1) Ordinary sub-luminal separation: B is exactly 5*escape/sep and is
|
|
# independent of the step count and of the initial step.
|
|
p_sub, _ = alc_map_prov('sub', 0.3, camera=alc_camera(0.3, 0.0))
|
|
assert p_sub['integrator'] == 1, p_sub
|
|
assert math.isclose(p_sub['max_lookback_time'],
|
|
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
|
|
assert p_sub['min_step'] <= p_sub['coordinate_time_step'] \
|
|
<= p_sub['max_step']
|
|
p_steps, _ = alc_map_prov('sub_steps', 0.3,
|
|
extra=('--trace-max-steps', '8'),
|
|
camera=alc_camera(0.3, 0.0))
|
|
assert p_steps['initial_max_steps'] == 8
|
|
assert math.isclose(p_steps['max_lookback_time'],
|
|
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
|
|
p_step, _ = alc_map_prov('sub_step', 0.3,
|
|
extra=('--ode-initial-step', '0.02'),
|
|
camera=alc_camera(0.3, 0.0))
|
|
assert abs(p_step['coordinate_time_step'] - 0.02) < 1e-15
|
|
assert math.isclose(p_step['max_lookback_time'],
|
|
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
|
|
|
|
# 2) Near-luminal (vs = 1, separation 0): the exp(-D) floor makes the
|
|
# allowance finite, and it grows with the dark threshold D.
|
|
p8, _ = alc_map_prov('near8', 1.0, D=8.0, camera=alc_camera(1.0, 0.0))
|
|
p12, _ = alc_map_prov('near12', 1.0, D=12.0, camera=alc_camera(1.0, 0.0))
|
|
assert math.isclose(p8['max_lookback_time'],
|
|
alcubierre_budget(escape1, 1.0, 8.0), rel_tol=1e-12)
|
|
assert math.isclose(p12['max_lookback_time'],
|
|
alcubierre_budget(escape1, 1.0, 12.0), rel_tol=1e-12)
|
|
assert p12['max_lookback_time'] > p8['max_lookback_time']
|
|
|
|
# 3) Both sides of the threshold-derived vcut and exactly luminal
|
|
# values use either separation or the finite floor; a tiny film traces
|
|
# and resolves without INCOMPLETE outcomes.
|
|
vcut = 1.0 - math.exp(-8.0)
|
|
for vs in (0.999, math.nextafter(vcut, 0.0),
|
|
math.nextafter(vcut, 1.0), 0.9999, math.nextafter(1.0, 0.0),
|
|
math.nextafter(1.0, 2.0)):
|
|
pv, mv = alc_map_prov(f'vcut_{vs!r}', vs, camera=alc_camera(vs, 0.0))
|
|
assert math.isclose(pv['max_lookback_time'],
|
|
alcubierre_budget(escape1, vs, 8.0),
|
|
rel_tol=1e-12), (vs, pv['max_lookback_time'])
|
|
vv, _ = map_vertices(mv)
|
|
assert 3 not in {v[10] for v in vv}, (vs, 'INCOMPLETE ray')
|
|
assert sum_rhs(vv) > 0, vs
|
|
|
|
# 4) The default camera (0,0,15 for R=5) must work with a superluminal
|
|
# bubble: the generic radius-15 camera lies inside escape radius 25.
|
|
pd, md = alc_map_prov('defaultcam', 2.0, radius=5.0)
|
|
dv, _ = map_vertices(md)
|
|
assert 3 not in {v[10] for v in dv}, 'default camera left INCOMPLETE rays'
|
|
assert sum_rhs(dv) > 0
|
|
|
|
# 5) Real small images at the critical velocities: the direction along
|
|
# the bubble motion is the DARK direction (Pi_x = sign(vs)); the
|
|
# central vertex must be DARK and some edge ray must escape. The
|
|
# 16x8 film with a 4-pixel coarse cell places a vertex exactly at the
|
|
# image center.
|
|
for vs in (1.0, -1.0, 2.0, -2.0):
|
|
ra = 0.0 if vs < 0.0 else 180.0
|
|
pimg, mimg = alc_map_prov(f'img_{vs!r}', vs, width=16, height=8,
|
|
cell=4, camera=alc_camera(vs, ra))
|
|
verts, _ = map_vertices(mimg)
|
|
outcomes = {v[10] for v in verts}
|
|
assert 3 not in outcomes, (vs, 'INCOMPLETE outcome', outcomes)
|
|
assert 0 in outcomes and 1 in outcomes, (vs, outcomes)
|
|
assert sum_rhs(verts) > 0, vs
|
|
central = min(verts, key=lambda v: (v[0] - 8.0) ** 2
|
|
+ (v[1] - 4.0) ** 2)
|
|
assert central[10] == 1, \
|
|
(vs, 'central vertex is not DARK', central[0], central[1],
|
|
central[10])
|
|
|
|
# 6) Extreme tiny-budget quota path: an explicit 4-step, 1-time
|
|
# allowance from an inside camera must stop as UNRESOLVED, never as
|
|
# a fabricated escape from a trace that took no steps.
|
|
extreme = (alc_camera(0.99999999, 0.0)
|
|
+ ['--alcubierre-vs', '0.99999999', '--alcubierre-radius',
|
|
'1', '--alcubierre-sigma', '1', '--catalog',
|
|
'assets/sky_grid_5deg.csv', '--width', '8', '--height',
|
|
'4', '--fov-deg', '80', '--exposure', '1e-3',
|
|
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
|
|
'--psf-relative-tail', '1e-4'])
|
|
ext_map = tmp / 'alc_extreme.grlens'
|
|
run(alc, *extreme, '--integrator', 'dp54',
|
|
'--trace-lookback-time', '1', '--trace-max-steps', '4',
|
|
'--max-total-steps', '4', '--retry-step-increment', '0',
|
|
'--max-total-lookback-time', '1', '--retry-lookback-increment', '0',
|
|
'--allow-incomplete', '--output', tmp / f'alc_extreme.{ext}',
|
|
'--lens-map-output', ext_map)
|
|
_, _, ext_prov = map_provenance(ext_map)
|
|
assert ext_prov['integrator'] == 1
|
|
assert abs(ext_prov['max_lookback_time'] - 1.0) < 1e-15, ext_prov
|
|
assert ext_prov['initial_max_steps'] == 4, ext_prov
|
|
ext_vertices, _ = map_vertices(ext_map)
|
|
assert all(v[10] == 2 for v in ext_vertices), \
|
|
('a 1-time/4-step trace fabricated a non-UNRESOLVED outcome',
|
|
[v[10] for v in ext_vertices])
|
|
|
|
# 7) Large dark threshold (D=1000) with vs=1: exp(-1000) underflows to
|
|
# 0 and the separation is exactly 0, so the log fallback saturates
|
|
# the default allowance to DBL_MAX/4. A 4-step/4-total cap with
|
|
# retry disabled keeps the trace short while the provenance records
|
|
# the non-masked saturated default. An explicit --trace-lookback
|
|
# overrides it without masking the independent step allowance.
|
|
psat, _ = alc_map_prov('sat1000', 1.0, D=1000.0,
|
|
extra=('--trace-max-steps', '4',
|
|
'--max-total-steps', '4',
|
|
'--retry-step-increment', '0'),
|
|
camera=alc_camera(1.0, 0.0))
|
|
assert math.isclose(psat['max_lookback_time'],
|
|
sys.float_info.max / 4.0, rel_tol=1e-12), \
|
|
psat['max_lookback_time']
|
|
assert psat['initial_max_steps'] == 4
|
|
psat_ov, _ = alc_map_prov('sat1000_ov', 1.0, D=1000.0,
|
|
extra=('--trace-lookback-time', '1',
|
|
'--trace-max-steps', '4',
|
|
'--max-total-steps', '4',
|
|
'--retry-step-increment', '0'),
|
|
camera=alc_camera(1.0, 0.0))
|
|
assert psat_ov['max_lookback_time'] == 1.0, psat_ov
|
|
assert psat_ov['initial_max_steps'] == 4, psat_ov
|
|
# Explicit time override must not mask the (default) step allowance.
|
|
pind, _ = alc_map_prov('indep', 1.0, D=8.0,
|
|
extra=('--trace-lookback-time', '1'),
|
|
camera=alc_camera(1.0, 0.0))
|
|
assert pind['max_lookback_time'] == 1.0, pind
|
|
assert pind['initial_max_steps'] > 1, pind
|
|
|
|
# 8) RK4 guard: a super-luminal default (vs=2, small estimate) is no
|
|
# longer rejected; the guard triggers only when the *derived default*
|
|
# estimate exceeds the cap (D=12, vs=1), and an explicit
|
|
# --trace-max-steps is a user allowance that bypasses it.
|
|
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '2',
|
|
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
|
|
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
|
|
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
|
|
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
|
|
'--psf-relative-tail', '1e-4', '--allow-incomplete',
|
|
'--output', tmp / f'alc_rk4_v2.{ext}')
|
|
rk4_guard = run(
|
|
alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
|
|
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
|
|
'--dark-threshold', '12', '--catalog', 'assets/sky_grid_5deg.csv',
|
|
'--width', '8', '--height', '4', '--fov-deg', '80',
|
|
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
|
|
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
|
|
'--output', tmp / f'alc_rk4_guard.{ext}', ok=False)
|
|
assert rk4_guard.returncode == 2, rk4_guard.stderr
|
|
assert 'cap' in rk4_guard.stderr, rk4_guard.stderr
|
|
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
|
|
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
|
|
'--dark-threshold', '12', '--trace-max-steps', '4',
|
|
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
|
|
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
|
|
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
|
|
'--psf-relative-tail', '1e-4', '--allow-incomplete',
|
|
'--output', tmp / f'alc_rk4_expl.{ext}')
|
|
|
|
# 9) Impossible parameters stay clearly rejected.
|
|
for bad in (['--alcubierre-vs', 'nan'], ['--alcubierre-vs', 'inf'],
|
|
['--alcubierre-radius', '0'], ['--alcubierre-sigma', '0']):
|
|
bad_out = tmp / f'alc_bad.{ext}'
|
|
rejected = run(alc, *bad, '--catalog', 'assets/sky_grid_5deg.csv',
|
|
'--width', '8', '--height', '4', '--fov-deg', '80',
|
|
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
|
|
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
|
|
'--output', bad_out, ok=False)
|
|
assert rejected.returncode != 0, rejected.stderr
|
|
assert not bad_out.exists()
|
|
|
|
print('alcubierre: budget/provenance, vcut scans, vcut images with '
|
|
'central DARK, default camera, large-D saturation, explicit '
|
|
'overrides and RK4 guard all passed', flush=True)
|