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.
This commit is contained in:
1 parent
c894fdb11a
commit
0a46a7095b
48 files changed
+11491
-380
No files matched your search
@@ -0,0 +1,545 @@
|
||||
#!/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 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(stderr, label):
|
||||
match = re.search(label + r' trace cost: accepted=(\d+) rejected=(\d+) '
|
||||
r'rhs=(\d+)', stderr)
|
||||
return None if match is None else match.groups()
|
||||
|
||||
|
||||
def sum_rhs(vertices):
|
||||
return sum(v[14] for v in vertices)
|
||||
|
||||
|
||||
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.stderr, 'Frame 0')
|
||||
replay_cost = trace_cost(replay_run.stderr, '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-default smoke: the DP54 default policy must
|
||||
# validate and the migrated lookback budget must actually cover the warp
|
||||
# bubble feature (rays integrate and escape) instead of pre-routing all
|
||||
# misses. No movie/thread sweep for this third backend.
|
||||
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'
|
||||
alc_map = tmp / 'alcubierre_default.grlens'
|
||||
alc_out = tmp / f'alcubierre_default.{ext}'
|
||||
run(alc, '--alcubierre-vs', 0.3, '--alcubierre-radius', 1,
|
||||
'--alcubierre-sigma', 1, '--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, '--verbose', '--output', alc_out,
|
||||
'--lens-map-output', alc_map)
|
||||
_, _, alc_prov = map_provenance(alc_map)
|
||||
assert alc_prov['integrator'] == 1, alc_prov
|
||||
assert alc_prov['min_step'] <= alc_prov['coordinate_time_step'] \
|
||||
<= alc_prov['max_step']
|
||||
assert alc_prov['max_lookback_time'] > 0
|
||||
alc_vertices, _ = map_vertices(alc_map)
|
||||
outcomes = {v[10] for v in alc_vertices}
|
||||
assert 3 not in outcomes, f'Alcubierre default left INCOMPLETE rays'
|
||||
assert any(v[10] == 0 for v in alc_vertices), \
|
||||
'Alcubierre default produced no escaped ray'
|
||||
assert sum_rhs(alc_vertices) > 0, \
|
||||
'Alcubierre default pre-routed every ray; lookback misses feature'
|
||||
assert image_payload(alc_out)
|
||||
|
||||
# The migrated coordinate-time coverage is the physical geometry budget
|
||||
# margin*4*escape/(1-|v_s|) with escape = R + 20/sigma, and it is
|
||||
# independent of the accepted-step count and of the chosen initial
|
||||
# step. Two cheap maps with different resource overrides must keep the
|
||||
# same lookback.
|
||||
alc_escape = 1.0 + 20.0 / 1.0 # R + 20/sigma for R=sigma=1
|
||||
alc_sep = 1.0 - abs(0.3)
|
||||
alc_expected_lookback = 1.25 * 4.0 * alc_escape / alc_sep
|
||||
assert abs(alc_prov['max_lookback_time'] - alc_expected_lookback) \
|
||||
< 1e-12, alc_prov['max_lookback_time']
|
||||
|
||||
def alc_map_with(tag, *options):
|
||||
m = tmp / f'alcubierre_{tag}.grlens'
|
||||
run(alc, '--alcubierre-vs', 0.3, '--alcubierre-radius', 1,
|
||||
'--alcubierre-sigma', 1, '--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, '--allow-incomplete',
|
||||
'--output', tmp / f'alcubierre_{tag}.{ext}',
|
||||
'--lens-map-output', m, *options)
|
||||
return map_provenance(m)[2]
|
||||
|
||||
steps_override = alc_map_with('steps_override', '--trace-max-steps', 8)
|
||||
assert steps_override['initial_max_steps'] == 8
|
||||
assert abs(steps_override['max_lookback_time'] - alc_expected_lookback) \
|
||||
< 1e-12, steps_override
|
||||
step_override = alc_map_with('step_override', '--ode-initial-step', 0.02)
|
||||
assert abs(step_override['coordinate_time_step'] - 0.02) < 1e-15
|
||||
assert abs(step_override['max_lookback_time'] - alc_expected_lookback) \
|
||||
< 1e-12, step_override
|
||||
|
||||
# Extreme separation (v_s = 0.99999999) where the legacy fixed-step
|
||||
# estimate far exceeds the cap. The DP path must accept an explicit
|
||||
# tiny coordinate-time budget instead of being rejected at startup by
|
||||
# the fixed-step guard, and it must actually exercise the quota path
|
||||
# (UNRESOLVED rays or real RHS work), not pre-route everything to
|
||||
# escapes. --allow-incomplete publishes the diagnostic frame.
|
||||
extreme = ['--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,
|
||||
'--observer-position', 0, 0, 0,
|
||||
'--observer-velocity', 0.99999999, 0, 0,
|
||||
'--look-ra-deg', 0, '--look-dec-deg', 0]
|
||||
ext_map = tmp / 'alcubierre_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'alcubierre_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 any(v[10] == 2 for v in ext_vertices) or \
|
||||
sum_rhs(ext_vertices) > 0, \
|
||||
'extreme Alcubierre case did not exercise the quota path'
|
||||
|
||||
# The same parameters without explicit DP budgets keep the legacy
|
||||
# fixed-step startup guard, which still rejects the estimated domain.
|
||||
rk4_extreme = run(
|
||||
alc, '--integrator', 'rk4', '--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,
|
||||
'--output', tmp / f'alcubierre_rk4_extreme.{ext}', ok=False)
|
||||
assert rk4_extreme.returncode == 2, rk4_extreme.stderr
|
||||
assert 'cap' in rk4_extreme.stderr, rk4_extreme.stderr
|
||||
|
||||
print('alcubierre: default DP config validated and produced escapes; '
|
||||
'extreme DP quota path ok, RK4 guard still rejects', flush=True)
|
||||
+52
-38
@@ -307,10 +307,11 @@ static void test_boundary_semantics_minkowski(void) {
|
||||
spacetime_destroy(&source);
|
||||
}
|
||||
|
||||
static void test_boundary_semantics_generic(void) {
|
||||
/* velocity_constant == 0 forces the generic bracketed driver. A finite
|
||||
* history bounds the outward/tangent searches, which must not be reported
|
||||
* as entries (they end as TIME_RANGE_EXHAUSTED instead). */
|
||||
static void test_nonconstant_route_unsupported(void) {
|
||||
/* velocity_constant == 0 signals an arbitrary accelerated worldtube. The
|
||||
* camera-inside containment test still runs first; once the ray starts
|
||||
* outside, the non-constant route is refused explicitly instead of being
|
||||
* searched with a coarse bracket. */
|
||||
SyntheticContext context = {.radius = 10.0,
|
||||
.valid_t_min = -100.0,
|
||||
.constant = 0};
|
||||
@@ -321,15 +322,15 @@ static void test_boundary_semantics_generic(void) {
|
||||
&source, &on_boundary, (double[]){-1.0, 0.0, 0.0}, &route);
|
||||
CHECK(inward == ASYMPTOTIC_OK &&
|
||||
route.kind == ASYMPTOTIC_ROUTE_INSIDE,
|
||||
"generic on-boundary inward is inside");
|
||||
"nonconstant on-boundary inward is still inside");
|
||||
const AsymptoticStatus outward = asymptotic_route_camera(
|
||||
&source, &on_boundary, (double[]){1.0, 0.0, 0.0}, &route);
|
||||
CHECK(outward == ASYMPTOTIC_TIME_RANGE_EXHAUSTED,
|
||||
"generic on-boundary outward is not an entry");
|
||||
CHECK(outward == ASYMPTOTIC_UNSUPPORTED,
|
||||
"nonconstant on-boundary outward is unsupported");
|
||||
const AsymptoticStatus tangent = asymptotic_route_camera(
|
||||
&source, &on_boundary, (double[]){0.0, 1.0, 0.0}, &route);
|
||||
CHECK(tangent == ASYMPTOTIC_TIME_RANGE_EXHAUSTED,
|
||||
"generic on-boundary tangent is not an entry");
|
||||
CHECK(tangent == ASYMPTOTIC_UNSUPPORTED,
|
||||
"nonconstant on-boundary tangent is unsupported");
|
||||
}
|
||||
|
||||
static void test_negative_radius_root_guard(void) {
|
||||
@@ -525,11 +526,12 @@ static void test_interior_crossing_bisection_failure(void) {
|
||||
"interior crossing bisection propagates history exhaustion");
|
||||
}
|
||||
|
||||
static void test_generic_bisection_failure(void) {
|
||||
/* The isolated invalid window lands on a bisection midpoint while the
|
||||
* bracket endpoints stay valid, so only the bisection can see it. With the
|
||||
* strict F < 0 entry test, the bracket is s = 80 (F == 0) to s = 90
|
||||
* (F < 0), so the first midpoint is t = -85. */
|
||||
static void test_nonconstant_preroute_unsupported(void) {
|
||||
/* The accelerated fixture is refused explicitly at the route entry; its
|
||||
* former coarse bracketed search (and the bisection-midpoint history-hole
|
||||
* case it exercised) is replaced by the unsupported contract. The history
|
||||
* reason itself stays covered by the constant and piecewise-constant
|
||||
* worldtube tests below. */
|
||||
SyntheticContext context = {.radius = 20.0,
|
||||
.valid_t_min = -1.0e30,
|
||||
.constant = 0,
|
||||
@@ -538,9 +540,26 @@ static void test_generic_bisection_failure(void) {
|
||||
SpacetimeSource source = {.ops = &synthetic_ops, .context = &context};
|
||||
const ObserverState camera = flat_observer(100.0, 0.0, 0.0);
|
||||
AsymptoticRoute route;
|
||||
CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0},
|
||||
&route) == ASYMPTOTIC_UNSUPPORTED,
|
||||
"nonconstant worldtube route is unsupported");
|
||||
}
|
||||
|
||||
static void test_piecewise_constant_history_hole(void) {
|
||||
/* Piecewise-constant motion stays supported, including its history
|
||||
* contract: when the segment walk steps past the valid history, the route
|
||||
* reports TIME_RANGE_EXHAUSTED and never disguises it as a miss. */
|
||||
SyntheticContext context = {.radius = 10.0,
|
||||
.valid_t_min = -3.0,
|
||||
.constant = 1,
|
||||
.segment_t = -5.0,
|
||||
.has_segment = 1};
|
||||
SpacetimeSource source = {.ops = &synthetic_ops, .context = &context};
|
||||
const ObserverState camera = flat_observer(50.0, 0.0, 0.0);
|
||||
AsymptoticRoute route;
|
||||
CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0},
|
||||
&route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED,
|
||||
"generic worldtube bisection propagates sample failure");
|
||||
"piecewise-constant segment history hole is exhausted");
|
||||
}
|
||||
|
||||
static void test_interior_history_exhaustion(void) {
|
||||
@@ -641,36 +660,30 @@ static void test_moving_sphere(void) {
|
||||
"co-moving ray misses");
|
||||
}
|
||||
|
||||
static void test_accelerated_worldtube(void) {
|
||||
static void test_accelerated_worldtube_unsupported(void) {
|
||||
/* A genuinely accelerating (non-constant velocity) worldtube has no strict
|
||||
* relative-motion interval bound, so the route is explicitly unsupported.
|
||||
* Its former finite-history branch is covered by the constant and
|
||||
* piecewise-constant history tests instead. */
|
||||
SyntheticContext context = {.vx = 0.0, .accel = 0.02, .radius = 20.0,
|
||||
.valid_t_min = -1.0e30, .constant = 0};
|
||||
SpacetimeSource source = {.ops = &synthetic_ops, .context = &context};
|
||||
const ObserverState camera = flat_observer(100.0, 0.0, 0.0);
|
||||
AsymptoticRoute route;
|
||||
CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0},
|
||||
&route) == ASYMPTOTIC_OK &&
|
||||
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
|
||||
"accelerated worldtube entry");
|
||||
double value;
|
||||
CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t,
|
||||
route.x, &value) == 0 &&
|
||||
fabs(value) <= 1e-13 * 20.0 * 20.0,
|
||||
"accelerated entry lies on worldtube");
|
||||
CHECK(route.activate_t < -40.0 && route.activate_t > -60.0,
|
||||
"accelerated entry time in range");
|
||||
|
||||
context.valid_t_min = -30.0;
|
||||
CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0},
|
||||
&route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED,
|
||||
"exhausted history is not a miss");
|
||||
&route) == ASYMPTOTIC_UNSUPPORTED,
|
||||
"accelerated worldtube route is unsupported");
|
||||
}
|
||||
|
||||
static void test_accelerated_segment(void) {
|
||||
static void test_piecewise_segment_entry(void) {
|
||||
/* The two-segment motion is piecewise constant, so velocity_constant == 1
|
||||
* per segment and the closed quadratic path must still find the entry in
|
||||
* the second segment (the analytic segmented first-entry case). */
|
||||
SyntheticContext context = {.vx = 0.0,
|
||||
.accel = 0.0,
|
||||
.radius = 20.0,
|
||||
.valid_t_min = -1.0e30,
|
||||
.constant = 0,
|
||||
.constant = 1,
|
||||
.segment_t = -50.0,
|
||||
.has_segment = 1};
|
||||
SpacetimeSource source = {.ops = &synthetic_ops, .context = &context};
|
||||
@@ -679,7 +692,7 @@ static void test_accelerated_segment(void) {
|
||||
CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0},
|
||||
&route) == ASYMPTOTIC_OK &&
|
||||
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
|
||||
"cross-segment entry");
|
||||
"piecewise cross-segment entry");
|
||||
CHECK(route.activate_t < context.segment_t,
|
||||
"entry lies past the motion-segment boundary");
|
||||
CHECK(fabs(route.activate_t + 65.0) < 1e-6, "cross-segment entry time");
|
||||
@@ -731,16 +744,17 @@ int main(void) {
|
||||
test_large_radius_quadratic();
|
||||
test_round_trip();
|
||||
test_moving_sphere();
|
||||
test_accelerated_worldtube();
|
||||
test_accelerated_segment();
|
||||
test_accelerated_worldtube_unsupported();
|
||||
test_piecewise_segment_entry();
|
||||
test_boundary_semantics_minkowski();
|
||||
test_boundary_semantics_generic();
|
||||
test_nonconstant_route_unsupported();
|
||||
test_negative_radius_root_guard();
|
||||
test_source_finalize();
|
||||
test_motion_segment_domain();
|
||||
test_schwarzschild_sample_failures();
|
||||
test_end_protocol_error();
|
||||
test_generic_bisection_failure();
|
||||
test_nonconstant_preroute_unsupported();
|
||||
test_piecewise_constant_history_hole();
|
||||
test_interior_history_exhaustion();
|
||||
test_interior_crossing_bisection_failure();
|
||||
test_ray_pool_lifecycle();
|
||||
|
||||
+25
-10
@@ -88,19 +88,31 @@ def image_payload(path, dimensions=(64, 48), allow_black=False):
|
||||
return raw
|
||||
|
||||
|
||||
# Version 3 wire layout. The first 100 bytes are the v2 provenance; the v3
|
||||
# adaptive policy appends 9 doubles and a u32 (76 bytes) so the provenance
|
||||
# block ends at 176. Each frame header is 48 bytes, so vertex payload starts at
|
||||
# 224. A v3 vertex keeps the 84-byte v2 record and appends three u64 cost
|
||||
# counters; a triangle stays 32 bytes. The CRC covers only the vertex+triangle
|
||||
# payload, never the header.
|
||||
MAP_PROVENANCE_END = 176
|
||||
MAP_FRAME_HEADER_START = MAP_PROVENANCE_END
|
||||
MAP_VERTEX_START = MAP_PROVENANCE_END + 48
|
||||
MAP_VERTEX_SIZE = 84 + 24
|
||||
MAP_TRIANGLE_SIZE = 32
|
||||
|
||||
|
||||
def map_vertices(path):
|
||||
data = path.read_bytes()
|
||||
assert data[:8] == b'GRLENS\x01\x00'
|
||||
assert struct.unpack_from('<I', data, 8)[0] == 3, 'expected v3 lens map'
|
||||
assert struct.unpack_from('<Q', data, 32)[0] == 1 # frame_count
|
||||
# Header: 40 bytes, then a 60-byte provenance block, then the 48-byte
|
||||
# per-frame header, so vertices start at offset 148 in the v2 format.
|
||||
vertices, triangles = struct.unpack_from('<QQ', data, 124)
|
||||
offset = 148
|
||||
vertices, triangles = struct.unpack_from('<QQ', data, MAP_FRAME_HEADER_START + 24)
|
||||
offset = MAP_VERTEX_START
|
||||
values = []
|
||||
for _ in range(vertices):
|
||||
values.append(struct.unpack_from('<9dIII', data, offset))
|
||||
offset += 84
|
||||
return values, data[offset:offset + triangles * 32]
|
||||
values.append(struct.unpack_from('<9dIIIQQQ', data, offset))
|
||||
offset += MAP_VERTEX_SIZE
|
||||
return values, data[offset:offset + triangles * MAP_TRIANGLE_SIZE]
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
@@ -282,7 +294,10 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
assert len(a) == len(b) and ta == tb
|
||||
max_error = 0
|
||||
for x, y in zip(a, b):
|
||||
assert x[9:] == y[9:], 'ray endpoint provenance mismatch'
|
||||
# Indices 9..11 are end_id/outcome/reason; indices 12..14 are the
|
||||
# per-vertex integration cost counters, which are not physical ray
|
||||
# provenance and may differ between the single and movie schedulers.
|
||||
assert x[9:12] == y[9:12], 'ray endpoint provenance mismatch'
|
||||
max_error = max(max_error, *(abs(v - w) for v, w in zip(x[:9], y[:9])))
|
||||
assert max_error < 1e-9, max_error
|
||||
|
||||
@@ -305,9 +320,9 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
# so this tests completion semantics rather than corruption detection.
|
||||
incomplete_map = tmp / f'{backend}_incomplete.grlens'
|
||||
payload = bytearray(single_map.read_bytes())
|
||||
struct.pack_into('<II', payload, 148 + 76, 3, 5) # INCOMPLETE / INVALID_METRIC
|
||||
struct.pack_into('<II', payload, MAP_VERTEX_START + 76, 3, 5) # INCOMPLETE / INVALID_METRIC
|
||||
struct.pack_into('<I', payload, len(payload)-4,
|
||||
zlib.crc32(payload[148:-4]))
|
||||
zlib.crc32(payload[MAP_VERTEX_START:-4]))
|
||||
incomplete_map.write_bytes(payload)
|
||||
refused = tmp / f'{backend}_refused.{ext}'
|
||||
failure = run(binary, *common, '--lens-map-input', incomplete_map,
|
||||
|
||||
+644
-2
@@ -9,6 +9,107 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Self-contained legacy v2 lens-map fixture writer. The production writer now
|
||||
* emits v3, so this serializes a real little-endian v2 map (v2 provenance, no
|
||||
* per-vertex cost counters) from a live mesh to keep the import path covered by
|
||||
* genuine bytes instead of a hand-maintained golden blob. */
|
||||
static uint32_t v2_crc32(uint32_t crc, const void *data, size_t size) {
|
||||
const unsigned char *bytes = data;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
crc ^= bytes[i];
|
||||
for (int bit = 0; bit < 8; ++bit)
|
||||
crc = (crc >> 1) ^ (0xedb88320u & (uint32_t)-(int)(crc & 1));
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
static int v2_fwrite_u32(FILE *f, uint32_t v) {
|
||||
unsigned char b[4] = {(unsigned char)v, (unsigned char)(v >> 8),
|
||||
(unsigned char)(v >> 16), (unsigned char)(v >> 24)};
|
||||
return fwrite(b, 1, sizeof b, f) == sizeof b ? 0 : -1;
|
||||
}
|
||||
static int v2_fwrite_u64(FILE *f, uint64_t v) {
|
||||
unsigned char b[8];
|
||||
for (int i = 0; i < 8; ++i) b[i] = (unsigned char)(v >> (8 * i));
|
||||
return fwrite(b, 1, sizeof b, f) == sizeof b ? 0 : -1;
|
||||
}
|
||||
static int v2_fwrite_double(FILE *f, double v) {
|
||||
uint64_t bits; memcpy(&bits, &v, sizeof bits);
|
||||
return v2_fwrite_u64(f, bits);
|
||||
}
|
||||
static int write_v2_lens_map(const char *path, int width, int height, double fov,
|
||||
const LensMapProvenance *p,
|
||||
const LensMapFrame *frame) {
|
||||
static const unsigned char magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0};
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (f == NULL) return -1;
|
||||
int failed = fwrite(magic, 1, sizeof magic, f) != sizeof magic ||
|
||||
v2_fwrite_u32(f, 2) || v2_fwrite_u32(f, 0x01020304u) ||
|
||||
v2_fwrite_u32(f, (uint32_t)width) || v2_fwrite_u32(f, (uint32_t)height) ||
|
||||
v2_fwrite_double(f, fov) || v2_fwrite_u64(f, 1) ||
|
||||
v2_fwrite_u32(f, p->threshold_kind) ||
|
||||
v2_fwrite_u32(f, p->threshold_policy_version) ||
|
||||
v2_fwrite_double(f, p->threshold_value) ||
|
||||
v2_fwrite_u32(f, p->retry_step_increment) ||
|
||||
v2_fwrite_u32(f, p->max_total_steps) || v2_fwrite_u32(f, p->max_level) ||
|
||||
v2_fwrite_u32(f, p->integrator) ||
|
||||
v2_fwrite_double(f, p->min_edge_pixels) ||
|
||||
v2_fwrite_double(f, p->min_area_pixels2) ||
|
||||
v2_fwrite_double(f, p->coordinate_time_step) ||
|
||||
v2_fwrite_u32(f, p->initial_max_steps);
|
||||
const FrameLensMesh *m = &frame->mesh;
|
||||
failed = failed || v2_fwrite_u64(f, frame->frame_id) ||
|
||||
v2_fwrite_double(f, frame->coordinate_time) ||
|
||||
v2_fwrite_double(f, frame->proper_time) ||
|
||||
v2_fwrite_u64(f, (uint64_t)m->vertex_count) ||
|
||||
v2_fwrite_u64(f, (uint64_t)m->triangle_count) ||
|
||||
v2_fwrite_u64(f, (uint64_t)m->retry_requests);
|
||||
/* The payload CRC covers vertices+triangles only; the header stays
|
||||
* deliberately independent, exactly as in the production writers. */
|
||||
uint32_t crc = UINT32_MAX;
|
||||
for (size_t i = 0; i < m->vertex_count; ++i) {
|
||||
const LensVertex *v = &m->vertices[i];
|
||||
const double vals[9] = {v->image_x, v->image_y, v->camera_direction[0],
|
||||
v->camera_direction[1], v->camera_direction[2],
|
||||
v->n_infinity[0], v->n_infinity[1], v->n_infinity[2],
|
||||
v->log_frequency_ratio};
|
||||
const uint32_t tail[3] = {(uint32_t)v->end_id, (uint32_t)v->outcome,
|
||||
(uint32_t)v->reason};
|
||||
unsigned char b[84]; size_t off = 0;
|
||||
for (int k = 0; k < 9; ++k) {
|
||||
uint64_t bits; memcpy(&bits, &vals[k], sizeof bits);
|
||||
for (int q = 0; q < 8; ++q) b[off++] = (unsigned char)(bits >> (8 * q));
|
||||
}
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
b[off++] = (unsigned char)tail[k];
|
||||
b[off++] = (unsigned char)(tail[k] >> 8);
|
||||
b[off++] = (unsigned char)(tail[k] >> 16);
|
||||
b[off++] = (unsigned char)(tail[k] >> 24);
|
||||
}
|
||||
if (fwrite(b, 1, sizeof b, f) != sizeof b) failed = 1;
|
||||
crc = v2_crc32(crc, b, sizeof b);
|
||||
}
|
||||
for (size_t i = 0; i < m->triangle_count; ++i) {
|
||||
unsigned char b[32]; size_t off = 0;
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const uint64_t idx = (uint64_t)m->triangles[i].vertex[j];
|
||||
for (int q = 0; q < 8; ++q) b[off++] = (unsigned char)(idx >> (8 * q));
|
||||
}
|
||||
const uint32_t tail[2] = {m->triangles[i].level,
|
||||
(uint32_t)m->triangles[i].approx_black};
|
||||
for (int k = 0; k < 2; ++k) {
|
||||
b[off++] = (unsigned char)tail[k];
|
||||
b[off++] = (unsigned char)(tail[k] >> 8);
|
||||
b[off++] = (unsigned char)(tail[k] >> 16);
|
||||
b[off++] = (unsigned char)(tail[k] >> 24);
|
||||
}
|
||||
if (fwrite(b, 1, sizeof b, f) != sizeof b) failed = 1;
|
||||
crc = v2_crc32(crc, b, sizeof b);
|
||||
}
|
||||
if (v2_fwrite_u32(f, crc ^ UINT32_MAX)) failed = 1;
|
||||
if (fclose(f)) failed = 1;
|
||||
return failed ? -1 : 0;
|
||||
}
|
||||
|
||||
static int mesh_has_hanging_vertex(const FrameLensMesh *mesh) {
|
||||
for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle)
|
||||
for (size_t side = 0; side < 3; ++side) {
|
||||
@@ -139,6 +240,9 @@ static int review_probe_regressions(void) {
|
||||
m.vertices[i].n_infinity[0]=1;
|
||||
}
|
||||
m.vertices[0].outcome=RAY_OUTCOME_UNRESOLVED;
|
||||
/* A genuinely budget-exhausted vertex has consumed its accepted-step
|
||||
* quota; the retry layer keys on that blocking quota. */
|
||||
m.vertices[0].continuation_steps=10;
|
||||
m.vertices[0].continuation_limit=10;
|
||||
if (frame_lens_mesh_prepare_generation(&m,&c)<1 || m.retry_requests==0)
|
||||
return -1; /* the retry counter is produced by real retry requests */
|
||||
@@ -153,11 +257,408 @@ static int review_probe_regressions(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A budget-exhausted vertex carrying the full adaptive resume state, so the
|
||||
* retry layer sees the same payload the production store_endpoint path writes. */
|
||||
static LensVertex unresolved_vertex(double image_x, double t, double start,
|
||||
unsigned int steps,
|
||||
unsigned int step_limit,
|
||||
double lookback_limit) {
|
||||
LensVertex v;
|
||||
memset(&v, 0, sizeof v);
|
||||
v.image_x = image_x;
|
||||
v.image_y = 0.0;
|
||||
v.camera_direction[0] = 1.0;
|
||||
v.outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
v.traced = 1;
|
||||
v.continuation_t = t;
|
||||
v.continuation_x[0] = 1.0;
|
||||
v.continuation_Pi[0] = -1.0;
|
||||
v.continuation_log_alpha_p0 = 0.125;
|
||||
v.continuation_log_alpha_p0_0 = 0.5;
|
||||
v.continuation_steps = steps;
|
||||
v.continuation_limit = step_limit;
|
||||
v.continuation_integration_start_time = start;
|
||||
v.continuation_next_step = 0.25;
|
||||
v.continuation_rejected_steps = 2;
|
||||
v.continuation_rhs_evaluations = 20;
|
||||
v.continuation_previous_rejected = 0;
|
||||
v.continuation_lookback_limit = lookback_limit;
|
||||
return v;
|
||||
}
|
||||
|
||||
static int uuu_fixture(LensVertex vertices[3], LensTriangle *triangle) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
vertices[i].camera_direction[0] = 1.0;
|
||||
*triangle = (LensTriangle){{0, 1, 2}, 0, 0, 0};
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Independent step/time retry budgets: a request is allowed only while every
|
||||
* quota that actually blocked the ray can still grow, and the two quotas are
|
||||
* saturated separately. Also verifies the resume-state round trip used by
|
||||
* both the frame and movie paths. */
|
||||
static int review_retry_quota_regressions(void) {
|
||||
/* 1. Step-blocked, both quotas growable: the step quota advances, and the
|
||||
* request records the independently saturated time quota. */
|
||||
{
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0),
|
||||
unresolved_vertex(10.0, -1.0, 0.0, 20, 20, 2.0),
|
||||
unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
RefinementConfig c = {.max_level = 0,
|
||||
.retry_step_increment = 10,
|
||||
.max_total_steps = 100,
|
||||
.retry_lookback_increment = 1.0,
|
||||
.max_total_lookback_time = 10.0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &c) != 3) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < m.sample_count; ++i)
|
||||
if (m.samples[i].kind != FRAME_SAMPLE_RETRY ||
|
||||
m.samples[i].step_limit != 30 ||
|
||||
m.samples[i].lookback_limit != 3.0) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
free(m.samples); free(m.probe_slots);
|
||||
}
|
||||
/* 2. Step quota at its cap while time can still grow: no request, because
|
||||
* more time cannot buy an accepted step. The UUU triangle stays a
|
||||
* budget-incomplete boundary, never blackened. */
|
||||
{
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0),
|
||||
unresolved_vertex(10.0, -1.0, 0.0, 20, 20, 2.0),
|
||||
unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
RefinementConfig c = {.max_level = 0,
|
||||
.retry_step_increment = 10,
|
||||
.max_total_steps = 20,
|
||||
.retry_lookback_increment = 1.0,
|
||||
.max_total_lookback_time = 10.0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &c) != 0) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
FrameBoundaryStats stats;
|
||||
frame_lens_mesh_boundary_stats(&m, &c, &stats);
|
||||
if (stats.uuu != 1 || stats.budget_incomplete_triangles == 0 ||
|
||||
stats.approx_black_triangles != 0) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
free(m.samples); free(m.probe_slots);
|
||||
}
|
||||
/* 3. Time-blocked with a disabled step increment: the time quota alone
|
||||
* enables the retry and the step budget is left untouched. */
|
||||
{
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, -3.0, 0.0, 5, 10, 2.0),
|
||||
unresolved_vertex(10.0, -3.0, 0.0, 5, 10, 2.0),
|
||||
unresolved_vertex(0.0, -3.0, 0.0, 5, 10, 2.0)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
RefinementConfig c = {.max_level = 0,
|
||||
.retry_step_increment = 0,
|
||||
.max_total_steps = 10,
|
||||
.retry_lookback_increment = 1.0,
|
||||
.max_total_lookback_time = 10.0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &c) != 3) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < m.sample_count; ++i)
|
||||
if (m.samples[i].kind != FRAME_SAMPLE_RETRY ||
|
||||
m.samples[i].step_limit != 10 ||
|
||||
m.samples[i].lookback_limit != 3.0) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
free(m.samples); free(m.probe_slots);
|
||||
}
|
||||
/* 4. Both blocking quotas at their caps: no request. */
|
||||
{
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, -6.0, 0.0, 20, 20, 5.0),
|
||||
unresolved_vertex(10.0, -6.0, 0.0, 20, 20, 5.0),
|
||||
unresolved_vertex(0.0, -6.0, 0.0, 20, 20, 5.0)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
RefinementConfig c = {.max_level = 0,
|
||||
.retry_step_increment = 10,
|
||||
.max_total_steps = 20,
|
||||
.retry_lookback_increment = 1.0,
|
||||
.max_total_lookback_time = 5.0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &c) != 0) {
|
||||
free(m.samples); free(m.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
free(m.samples); free(m.probe_slots);
|
||||
}
|
||||
/* 5. Resume-state round trip: the helper reproduces every control field, and
|
||||
* install back-fills the vertex with the endpoint's new state and quota. */
|
||||
{
|
||||
LensVertex source = unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0);
|
||||
GeodesicRayState state;
|
||||
if (frame_vertex_continuation_state(&source, &state) ||
|
||||
state.coordinate_time != -1.0 || state.steps != 20 ||
|
||||
state.integration_start_time != 0.0 || state.next_step != 0.25 ||
|
||||
state.rejected_steps != 2 || state.rhs_evaluations != 20 ||
|
||||
state.previous_rejected != 0 || state.log_alpha_p0 != 0.125 ||
|
||||
state.log_alpha_p0_0 != 0.5 || state.x[0] != 1.0 ||
|
||||
state.Pi[0] != -1.0)
|
||||
return -1;
|
||||
LensVertex target = {.image_x = 0.0, .camera_direction = {1.0, 0.0, 0.0}};
|
||||
FrameLensMesh mesh = {.vertices = &target, .vertex_count = 1};
|
||||
RefinementConfig plain = {.max_level = 0};
|
||||
if (frame_lens_mesh_prepare_generation(&mesh, &plain) != 1) {
|
||||
free(mesh.samples); free(mesh.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
const RayEndpoint endpoint = {
|
||||
.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.reason = RAY_REASON_BUDGET_EXHAUSTED,
|
||||
.stop_coordinate_time = -4.0,
|
||||
.accepted_steps = 7,
|
||||
.final_x = {5.0, 0.0, 0.0},
|
||||
.final_Pi = {-1.0, 0.0, 0.0},
|
||||
.final_log_alpha_p0 = 0.125,
|
||||
.final_log_alpha_p0_0 = 0.5,
|
||||
.threshold_value = NAN,
|
||||
.integration_start_time = 0.0,
|
||||
.next_step = 0.5,
|
||||
.rejected_steps = 3,
|
||||
.rhs_evaluations = 70,
|
||||
.previous_rejected = 1,
|
||||
.lookback_limit = 2.5};
|
||||
if (frame_lens_mesh_install_sample(&mesh, 0, &endpoint) ||
|
||||
target.outcome != RAY_OUTCOME_UNRESOLVED ||
|
||||
target.continuation_t != -4.0 || target.continuation_steps != 7 ||
|
||||
target.continuation_limit != 7 ||
|
||||
target.continuation_integration_start_time != 0.0 ||
|
||||
target.continuation_next_step != 0.5 ||
|
||||
target.continuation_rejected_steps != 3 ||
|
||||
target.continuation_rhs_evaluations != 70 ||
|
||||
target.continuation_previous_rejected != 1 ||
|
||||
target.continuation_lookback_limit != 2.5) {
|
||||
free(mesh.samples); free(mesh.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
GeodesicRayState rebuilt;
|
||||
if (frame_vertex_continuation_state(&target, &rebuilt) ||
|
||||
rebuilt.coordinate_time != -4.0 || rebuilt.steps != 7 ||
|
||||
rebuilt.next_step != 0.5 || rebuilt.rejected_steps != 3 ||
|
||||
rebuilt.rhs_evaluations != 70 || rebuilt.previous_rejected != 1) {
|
||||
free(mesh.samples); free(mesh.probe_slots);
|
||||
return -1;
|
||||
}
|
||||
free(mesh.samples); free(mesh.probe_slots);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static GeodesicTraceConfig frame_dp_config(double lookback) {
|
||||
GeodesicTraceConfig c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.stepper = GEODESIC_STEPPER_DP54;
|
||||
c.coordinate_time_step = 0.5;
|
||||
c.max_steps = 100;
|
||||
c.threshold = (ThresholdPolicy){.kind = THRESHOLD_DISABLED, .value = 0.0,
|
||||
.policy_version = 0};
|
||||
c.atol_x = c.atol_Pi = c.atol_L = c.rtol = 1e-9;
|
||||
c.min_step = 1e-12;
|
||||
c.max_step = 1e6;
|
||||
c.consecutive_rejection_limit = 1000;
|
||||
c.max_lookback_time = lookback;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* A real production trace must record the configured step/time grant distinct
|
||||
* from the spent counts, so a time-limited ray can retry with extra time while
|
||||
* keeping its full step grant. A retry candidate also counts only when it
|
||||
* opens a strictly larger representable region. */
|
||||
static int review_real_grant_and_time_growth(void) {
|
||||
SpacetimeSource source = {0};
|
||||
if (spacetime_create_minkowski(&source, 10.0))
|
||||
return -1;
|
||||
const ObserverState observer = observer_fixed_at_origin();
|
||||
int failed = 0;
|
||||
|
||||
/* A. Real trace at the origin, time-limited after ~2 of 100 granted steps.
|
||||
* With the step increment disabled and the time increment enabled, the
|
||||
* retry must keep step_limit == 100 and only grow the time budget. */
|
||||
{
|
||||
LensVertex vertices[3] = {{.camera_direction = {1.0, 0.0, 0.0}},
|
||||
{.camera_direction = {1.0, 0.0, 0.0}},
|
||||
{.camera_direction = {1.0, 0.0, 0.0}}};
|
||||
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
const GeodesicTraceConfig trace = frame_dp_config(1.0);
|
||||
if (frame_lens_mesh_trace(&m, &source, &observer, &trace))
|
||||
failed = 1;
|
||||
for (size_t i = 0; i < 3 && !failed; ++i) {
|
||||
if (vertices[i].outcome != RAY_OUTCOME_UNRESOLVED ||
|
||||
vertices[i].continuation_limit != 100u ||
|
||||
vertices[i].continuation_steps >= 100u ||
|
||||
vertices[i].continuation_lookback_limit != 1.0)
|
||||
failed = 1;
|
||||
}
|
||||
const RefinementConfig retry = {.max_level = 0,
|
||||
.retry_step_increment = 0,
|
||||
.max_total_steps = 100,
|
||||
.retry_lookback_increment = 1.0,
|
||||
.max_total_lookback_time = 100.0};
|
||||
if (!failed && frame_lens_mesh_prepare_generation(&m, &retry) != 3)
|
||||
failed = 1;
|
||||
for (size_t i = 0; i < m.sample_count && !failed; ++i)
|
||||
if (m.samples[i].kind != FRAME_SAMPLE_RETRY ||
|
||||
m.samples[i].step_limit != 100u ||
|
||||
m.samples[i].lookback_limit != 2.0)
|
||||
failed = 1;
|
||||
free(m.samples);
|
||||
free(m.probe_slots);
|
||||
}
|
||||
|
||||
/* B. Translated time origins (positive, negative and zero) still detect the
|
||||
* lookback boundary with the integrator's own comparison and retry with a
|
||||
* strictly larger time region while preserving the step grant. */
|
||||
for (int origin = 0; origin < 3 && !failed; ++origin) {
|
||||
const double t0 = origin == 0 ? 0.0 : origin == 1 ? 1.0e9 : -1.0e9;
|
||||
ObserverState o = observer_fixed_at_origin();
|
||||
o.coordinate_time = t0;
|
||||
const GeodesicTraceConfig trace = frame_dp_config(0.3);
|
||||
MetricData metric;
|
||||
if (spacetime_eval(&source, t0, o.coordinate_position, &metric)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
GeodesicRayState state;
|
||||
if (geodesic_initialize_past_ray_metric(&metric, &o, (double[]){1, 0, 0},
|
||||
&state)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
const RayEndpoint endpoint =
|
||||
geodesic_trace_past_from_state(&source, &state, &trace);
|
||||
if (endpoint.outcome != RAY_OUTCOME_UNRESOLVED ||
|
||||
endpoint.accepted_step_limit != 100u || endpoint.accepted_steps != 1u) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
LensVertex vertices[3];
|
||||
memset(vertices, 0, sizeof vertices);
|
||||
vertices[1].traced = vertices[2].traced = 1;
|
||||
vertices[1].outcome = vertices[2].outcome = RAY_OUTCOME_ESCAPED;
|
||||
vertices[1].n_infinity[0] = vertices[2].n_infinity[0] = 1.0;
|
||||
vertices[1].end_id = vertices[2].end_id = 0;
|
||||
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
const RefinementConfig plain = {.max_level = 0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &plain) != 1 ||
|
||||
frame_lens_mesh_install_sample(&m, 0, &endpoint) ||
|
||||
vertices[0].continuation_limit != 100u ||
|
||||
vertices[0].continuation_lookback_limit != 0.3) {
|
||||
free(m.samples);
|
||||
free(m.probe_slots);
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
free(m.samples);
|
||||
m.samples = NULL;
|
||||
m.sample_count = m.sample_capacity = 0;
|
||||
const RefinementConfig retry = {.max_level = 0,
|
||||
.retry_step_increment = 0,
|
||||
.max_total_steps = 100,
|
||||
.retry_lookback_increment = 0.3,
|
||||
.max_total_lookback_time = 10.0};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &retry) != 1 ||
|
||||
m.samples[0].kind != FRAME_SAMPLE_RETRY ||
|
||||
m.samples[0].step_limit != 100u ||
|
||||
m.samples[0].lookback_limit != 0.6)
|
||||
failed = 1;
|
||||
free(m.samples);
|
||||
free(m.probe_slots);
|
||||
}
|
||||
|
||||
/* C. A larger quota that does not move the left boundary (increment rounds
|
||||
* away, or a large time origin absorbs it) must not launch a retry. */
|
||||
{
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, -1.0e9, 0.0, 1, 100, 1.0e9),
|
||||
unresolved_vertex(10.0, -1.0e9, 0.0, 1, 100, 1.0e9),
|
||||
unresolved_vertex(0.0, -1.0e9, 0.0, 1, 100, 1.0e9)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
const RefinementConfig rounded = {.max_level = 0,
|
||||
.retry_step_increment = 0,
|
||||
.max_total_steps = 100,
|
||||
.retry_lookback_increment = 1.0e-8,
|
||||
.max_total_lookback_time = 2.0e9};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &rounded) != 0)
|
||||
failed = 1;
|
||||
FrameBoundaryStats stats;
|
||||
frame_lens_mesh_boundary_stats(&m, &rounded, &stats);
|
||||
if (stats.uuu != 1 || stats.budget_incomplete_triangles == 0)
|
||||
failed = 1;
|
||||
free(m.samples);
|
||||
free(m.probe_slots);
|
||||
}
|
||||
{
|
||||
const double start = 1.0e12;
|
||||
const double quota = 1.0e6;
|
||||
LensVertex vertices[3] = {
|
||||
unresolved_vertex(0.0, start - quota, start, 1, 100, quota),
|
||||
unresolved_vertex(10.0, start - quota, start, 1, 100, quota),
|
||||
unresolved_vertex(0.0, start - quota, start, 1, 100, quota)};
|
||||
LensTriangle triangle;
|
||||
uuu_fixture(vertices, &triangle);
|
||||
FrameLensMesh m = {.vertices = vertices, .vertex_count = 3,
|
||||
.triangles = &triangle, .triangle_count = 1};
|
||||
const RefinementConfig absorbed = {.max_level = 0,
|
||||
.retry_step_increment = 0,
|
||||
.max_total_steps = 100,
|
||||
.retry_lookback_increment = 1.0e-6,
|
||||
.max_total_lookback_time = 2.0e6};
|
||||
if (frame_lens_mesh_prepare_generation(&m, &absorbed) != 0)
|
||||
failed = 1;
|
||||
free(m.samples);
|
||||
free(m.probe_slots);
|
||||
}
|
||||
spacetime_destroy(&source);
|
||||
return failed ? -1 : 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (review_probe_regressions()) {
|
||||
fputs("probe persistence / retry invalidation regression failed\n",stderr);
|
||||
return 1;
|
||||
}
|
||||
if (review_retry_quota_regressions()) {
|
||||
fputs("independent retry quota regression failed\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
if (review_real_grant_and_time_growth()) {
|
||||
fputs("real grant / representable time growth regression failed\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
const int width = 100, height = 100;
|
||||
const double test_exposure = 1e-3;
|
||||
const double psf_relative_tail = 1e-8;
|
||||
@@ -187,7 +688,7 @@ int main(void) {
|
||||
}
|
||||
/* A finalized mesh can be persisted independently of spacetime and then
|
||||
* drive the exact same catalog inverse-map and PSF pass. */
|
||||
const char *lens_map_path = "/tmp/gr_lens_map_test.grlens";
|
||||
const char *lens_map_path = "/tmp/opencode/gr_lens_map_test.grlens";
|
||||
mesh.retry_requests = 2; /* cumulative per-frame retry accounting round-trips */
|
||||
const LensMapFrame saved_frame = {.frame_id = 7,
|
||||
.coordinate_time = 3.0,
|
||||
@@ -259,7 +760,7 @@ int main(void) {
|
||||
/* A version-1 header must be rejected outright: its captured bit cannot be
|
||||
* upgraded into the new dark/unresolved/error provenance. */
|
||||
{
|
||||
const char *legacy_path = "/tmp/gr_lens_map_v1_test.grlens";
|
||||
const char *legacy_path = "/tmp/opencode/gr_lens_map_v1_test.grlens";
|
||||
FILE *legacy = fopen(legacy_path, "wb");
|
||||
int legacy_failed = legacy == NULL;
|
||||
if (!legacy_failed) {
|
||||
@@ -906,6 +1407,147 @@ int main(void) {
|
||||
}
|
||||
free(ud_mesh.probe_slots);
|
||||
}
|
||||
/* v3 DP54 round-trip: every adaptive/quota field and the per-vertex cost
|
||||
* counters must survive the wire exactly. */
|
||||
{
|
||||
const char *dp_path = "/tmp/opencode/gr_lens_map_v3_dp_test.grlens";
|
||||
LensVertex dv[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
dv[i] = (LensVertex){.image_x = (double)i, .image_y = 2.0,
|
||||
.camera_direction = {0.0, 0.0, -1.0},
|
||||
.outcome = RAY_OUTCOME_DARK,
|
||||
.reason = RAY_REASON_REDSHIFT_LIMIT,
|
||||
.end_id = SPACETIME_END_NONE, .traced = 1};
|
||||
dv[0].trace_accepted_steps = 11; dv[0].trace_rejected_steps = 2;
|
||||
dv[0].trace_rhs_evaluations = 79;
|
||||
dv[1].trace_accepted_steps = 5;
|
||||
LensTriangle dt = {{0, 1, 2}, 1, 1, 0};
|
||||
FrameLensMesh dm = {.vertices = dv, .triangles = &dt, .vertex_count = 3,
|
||||
.vertex_capacity = 3, .triangle_count = 1,
|
||||
.triangle_capacity = 1};
|
||||
LensMapFrame df = {.frame_id = 3, .coordinate_time = 1.5,
|
||||
.proper_time = 1.25, .mesh = dm};
|
||||
const LensMapProvenance dp = {.threshold_kind = THRESHOLD_LOG_ENERGY_GROWTH,
|
||||
.threshold_policy_version = 3, .threshold_value = 8.0,
|
||||
.retry_step_increment = 64, .max_total_steps = 256, .max_level = 2,
|
||||
.integrator = (uint32_t)GEODESIC_STEPPER_DP54,
|
||||
.min_edge_pixels = 0.5, .min_area_pixels2 = 0.25,
|
||||
.coordinate_time_step = 0.1, .initial_max_steps = 1024,
|
||||
.atol_x = 1e-9, .atol_Pi = 1e-9, .atol_L = 1e-9, .rtol = 1e-9,
|
||||
.min_step = 1e-12, .max_step = 2.0, .max_lookback_time = 102.4,
|
||||
.retry_lookback_increment = 102.4, .max_total_lookback_time = 409.6,
|
||||
.max_consecutive_rejections = 32};
|
||||
LensMap dloaded = {0};
|
||||
if (lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1) ||
|
||||
lens_map_read(dp_path, NULL, &dloaded)) {
|
||||
fputs("lens-map v3 DP54 round-trip regression failed\n", stderr);
|
||||
lens_map_destroy(&dloaded); unlink(dp_path); goto done;
|
||||
}
|
||||
const LensMapProvenance *lp = &dloaded.provenance;
|
||||
if (dloaded.file_version != 3 ||
|
||||
lp->integrator != (uint32_t)GEODESIC_STEPPER_DP54 ||
|
||||
lp->atol_x != 1e-9 || lp->atol_Pi != 1e-9 || lp->atol_L != 1e-9 ||
|
||||
lp->rtol != 1e-9 || lp->min_step != 1e-12 || lp->max_step != 2.0 ||
|
||||
lp->max_lookback_time != 102.4 ||
|
||||
lp->retry_lookback_increment != 102.4 ||
|
||||
lp->max_total_lookback_time != 409.6 ||
|
||||
lp->max_consecutive_rejections != 32 ||
|
||||
lp->retry_step_increment != 64 || lp->max_total_steps != 256 ||
|
||||
lp->coordinate_time_step != 0.1 || lp->initial_max_steps != 1024 ||
|
||||
dloaded.frames[0].mesh.vertices[0].trace_accepted_steps != 11 ||
|
||||
dloaded.frames[0].mesh.vertices[0].trace_rejected_steps != 2 ||
|
||||
dloaded.frames[0].mesh.vertices[0].trace_rhs_evaluations != 79 ||
|
||||
dloaded.frames[0].mesh.vertices[1].trace_accepted_steps != 5 ||
|
||||
dloaded.frames[0].mesh.triangles[0].level != 1) {
|
||||
fputs("lens-map v3 DP54 field round-trip regression failed\n", stderr);
|
||||
lens_map_destroy(&dloaded); unlink(dp_path); goto done;
|
||||
}
|
||||
lens_map_destroy(&dloaded);
|
||||
/* Unknown wire code and non-finite/out-of-bounds DP fields must be rejected
|
||||
* by the shared schema validator, not accepted as a usable map. */
|
||||
{
|
||||
/* offset, is_double, double_value, u32_value */
|
||||
const struct { long offset; int is_double; double dvalue; uint32_t uvalue; }
|
||||
corruptions[4] = {{68, 0, 0.0, 7u}, /* unknown integrator code */
|
||||
{100, 1, NAN, 0u}, /* atol_x = NaN */
|
||||
{108, 1, -1.0, 0u}, /* atol_Pi below zero */
|
||||
{156, 1, -1.0, 0u}}; /* max_lookback below zero */
|
||||
for (size_t c = 0; c < 4; ++c) {
|
||||
if (lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1)) {
|
||||
fputs("lens-map v3 corruption fixture write failed\n", stderr);
|
||||
unlink(dp_path); goto done;
|
||||
}
|
||||
FILE *bad = fopen(dp_path, "r+b");
|
||||
int bad_failed =
|
||||
bad == NULL || fseek(bad, corruptions[c].offset, SEEK_SET);
|
||||
if (!bad_failed) {
|
||||
if (corruptions[c].is_double)
|
||||
bad_failed = fwrite(&corruptions[c].dvalue,
|
||||
sizeof(double), 1, bad) != 1;
|
||||
else
|
||||
bad_failed = fwrite(&corruptions[c].uvalue,
|
||||
sizeof(uint32_t), 1, bad) != 1;
|
||||
}
|
||||
if (bad != NULL && fclose(bad)) bad_failed = 1;
|
||||
if (bad_failed || !lens_map_read(dp_path, NULL, &dloaded)) {
|
||||
fputs("lens-map v3 invalid DP54 field rejection regression failed\n",
|
||||
stderr);
|
||||
lens_map_destroy(&dloaded); unlink(dp_path); goto done;
|
||||
}
|
||||
lens_map_destroy(&dloaded);
|
||||
}
|
||||
}
|
||||
unlink(dp_path);
|
||||
}
|
||||
/* Legacy v2 import: a real v2 map (no adaptive fields, no cost counters)
|
||||
* must load as RK4 with an explicit zero adaptive policy and render
|
||||
* identically to the live mesh. */
|
||||
{
|
||||
const char *v2_path = "/tmp/opencode/gr_lens_map_v2_legacy_test.grlens";
|
||||
const LensMapProvenance v2p = {.threshold_kind = THRESHOLD_LOG_ALPHA_P0,
|
||||
.threshold_policy_version = 1, .threshold_value = 8.0,
|
||||
.retry_step_increment = 16, .max_total_steps = 64, .max_level = 2,
|
||||
.integrator = (uint32_t)GEODESIC_STEPPER_RK4,
|
||||
.min_edge_pixels = 0.5, .min_area_pixels2 = 0.25,
|
||||
.coordinate_time_step = 0.1, .initial_max_steps = 4096};
|
||||
const LensMapFrame v2f = {.frame_id = 7, .coordinate_time = 3.0,
|
||||
.proper_time = 2.0, .mesh = mesh};
|
||||
LensMap v2loaded = {0};
|
||||
double *v2_hdr = calloc((size_t)width * height * 3, sizeof *v2_hdr);
|
||||
double *live_hdr = calloc((size_t)width * height * 3, sizeof *live_hdr);
|
||||
if (v2_hdr == NULL || live_hdr == NULL ||
|
||||
write_v2_lens_map(v2_path, width, height, 30.0, &v2p, &v2f) ||
|
||||
lens_map_read(v2_path, NULL, &v2loaded) ||
|
||||
v2loaded.file_version != 2 ||
|
||||
v2loaded.provenance.integrator != (uint32_t)GEODESIC_STEPPER_RK4 ||
|
||||
v2loaded.provenance.atol_x != 0.0 ||
|
||||
v2loaded.provenance.max_lookback_time != 0.0 ||
|
||||
v2loaded.provenance.max_total_lookback_time != 0.0 ||
|
||||
v2loaded.provenance.max_consecutive_rejections != 0 ||
|
||||
v2loaded.frames[0].mesh.vertices[0].trace_accepted_steps != 0 ||
|
||||
v2loaded.frames[0].mesh.vertices[0].trace_rhs_evaluations != 0) {
|
||||
fputs("lens-map v2 legacy import regression failed\n", stderr);
|
||||
free(v2_hdr); free(live_hdr); lens_map_destroy(&v2loaded);
|
||||
unlink(v2_path); goto done;
|
||||
}
|
||||
const size_t live_images = frame_splat_catalog(
|
||||
&mesh, &catalog, live_hdr, width, height, test_exposure, &psf, NULL,
|
||||
INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1,
|
||||
FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL, NULL);
|
||||
const size_t v2_images = frame_splat_catalog(
|
||||
&v2loaded.frames[0].mesh, &catalog, v2_hdr, width, height,
|
||||
test_exposure, &psf, NULL, INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1,
|
||||
FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL, NULL);
|
||||
int render_equal = live_images == v2_images && live_images == images;
|
||||
for (int k = 0; render_equal && k < width * height * 3; ++k)
|
||||
if (live_hdr[k] != v2_hdr[k]) render_equal = 0;
|
||||
free(v2_hdr); free(live_hdr);
|
||||
lens_map_destroy(&v2loaded); unlink(v2_path);
|
||||
if (!render_equal) {
|
||||
fputs("lens-map v2 legacy render mismatch regression failed\n", stderr);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
result = 0;
|
||||
done:
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user