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:
1 parent
7fde49308b
commit
f7380cbf75
31 files changed
+2349
-402
No files matched your search
+39
-10
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user