Feat: Rework ray termination into escaped/dark/unresolved/incomplete

Replace the position capture cutoff with a camera-relative dark threshold
shared by every backend, and carry explicit outcome/reason provenance
through the ray, RayPool, adaptive mesh, lens-map and replay paths.

- eval/eval_slab return SpacetimePointStatus; remove SPACETIME_RAY_CAPTURED
  and the Schwarzschild capture radius; decouple observer construction from
  ray position.
- RayEndpoint stores RayOutcome/RayReason plus the last trusted state;
  budget exhaustion is retryable UNRESOLVED, data/integration failures are
  INCOMPLETE.
- Normal dark terminal is L - L0 >= --dark-threshold (default 8), with L0
  taken at the camera event and kept distinct from the worldtube entry
  energy; photon energy and frequency ratio are never reset.
- Implement E/D/U triangle decisions with merged budget retries, persistent
  probe witnesses promoted in place by vertex identity, conformity settling,
  and approximate-black boundary provenance with achieved-scale statistics.
- Add RayPool continuation state and per-ray step budgets.
- Bump lens-map to v2 with explicit end/outcome/reason, approx_black,
  threshold/retry/geometry provenance and per-frame retry counts; reject v1.
- Gate production output on incomplete/error results, overridable with
  --allow-incomplete.
- Update AGENTS.md, the design document and usage docs; add the termination
  oracle and regression coverage.

make -B -j4 BUILD_TYPE=Debug test passes with bit-identical reference HDRs.
This commit is contained in:
wyj committed 2026-10-05 06:22:47 -04:00
1 parent 7fde49308b
commit f7380cbf75
31 files changed
+2349 -402

No files matched your search

+39 -10
View File
@@ -91,14 +91,16 @@ def image_payload(path, dimensions=(64, 48), allow_black=False):
def map_vertices(path):
data = path.read_bytes()
assert data[:8] == b'GRLENS\x01\x00'
assert struct.unpack_from('<Q', data, 32)[0] == 1
vertices, triangles = struct.unpack_from('<QQ', data, 64)
offset = 80
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
values = []
for _ in range(vertices):
values.append(struct.unpack_from('<9dI', data, offset))
offset += 76
return values, data[offset:offset + triangles * 28]
values.append(struct.unpack_from('<9dIII', data, offset))
offset += 84
return values, data[offset:offset + triangles * 32]
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
@@ -113,6 +115,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
assert '--tone-map' in help_text and '--tone-map-p' in help_text
assert '--sensor-bloom-limit' in help_text
assert '--sensor-bloom-transfer' in help_text
assert '--dark-threshold' in help_text
assert '--observer-inward-speed' not in help_text
assert '_mesh.' in help_text, help_text
# The synthetic grid is calibrated for the renderer's default exposure.
@@ -240,12 +243,20 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 1.5], None),
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 'nan'], None),
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 'inf'], None),
(['--dark-threshold', 'nan'], None),
(['--dark-threshold', 'inf'], None),
(['--dark-threshold', 0], None),
(['--dark-threshold', -1], None),
]
if backend == 'schwarzschild':
errors += [(['--observer-position', 1.5, 0, 0, '--observer-velocity', -0.5, 0, 0], 'capture cutoff'),
(['--observer-position', 1.75, 0, 0], 'not timelike')]
errors += [(['--observer-position', 1.75, 0, 0], 'not timelike')]
render('inside', '--observer-position', 1.75, 0, 0,
'--observer-velocity', -0.5, 0, 0, '--look-ra-deg', 0, '--look-dec-deg', 0)
# A camera inside the old r=1.5 capture cutoff is a normal target.
render('inside_cutoff', '--observer-position', 1.5, 0, 0,
'--observer-velocity', -0.5, 0, 0, '--look-ra-deg', 0, '--look-dec-deg', 0)
# The camera-relative dark threshold is configurable.
render('dark_threshold_10', '--dark-threshold', 10)
for options, message in errors:
missing_catalog = tmp / 'should_not_be_created.csv'
result = run(binary, '--catalog', missing_catalog, *options, ok=False)
@@ -271,8 +282,8 @@ 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[-1] == y[-1], 'ray classification mismatch'
max_error = max(max_error, *(abs(v - w) for v, w in zip(x[:-1], y[:-1])))
assert x[9:] == y[9:], '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
# Movie frames keep the clean primary image and gain correctly named
@@ -289,6 +300,24 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
# A map import must still work without evaluating a camera/metric, and
# must follow the same clean-main plus mesh-sibling rule.
assert single == render('import', '--lens-map-input', single_map)
# A structurally valid v2 map with an explicit metric failure must not
# bypass the live-tracing publication gate. Recompute the payload CRC
# 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('<I', payload, len(payload)-4,
zlib.crc32(payload[148:-4]))
incomplete_map.write_bytes(payload)
refused = tmp / f'{backend}_refused.{ext}'
failure = run(binary, *common, '--lens-map-input', incomplete_map,
'--output', refused, ok=False)
assert 'Incomplete render refused' in failure.stderr
assert not refused.exists()
diagnostic = run(binary, *common, '--lens-map-input', incomplete_map,
'--allow-incomplete', '--output', refused)
assert 'publishing incomplete render' in diagnostic.stderr
assert refused.exists()
imported_mesh = tmp / f'{backend}_import_mesh.{ext}'
run(binary, *common, '--lens-map-input', single_map, '--draw-mesh',
'--output', imported_mesh)