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

+4 -4
View File
@@ -113,7 +113,7 @@ int main(int argc, char **argv) {
/* Ingoing radial light seen from the horizon and its interior must still
* trace backwards to the external sky, rather than be classified captured. */
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.05,
.max_steps = 8192, .capture_log_alpha_p0 = 8};
.max_steps = 8192, .threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH, .value = 8.0, .policy_version = 3}};
for (int i = 0; i < 3; ++i) {
camera = (ObserverCamera){.position = {2.25 - 0.25 * i, 0, 0},
.velocity = {-0.5, 0, 0}};
@@ -121,7 +121,7 @@ int main(int argc, char **argv) {
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
CHECK(check_state(&metric, &camera, &state) == 0);
const RayEndpoint ray = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
CHECK(fabs(ray.n_infinity[0] - 1) < 1e-12);
/* Radial ingoing KS photon has k^r=-k^t and conserved E=k^t. The
* asymptotic exterior transfers the photon to infinity, where
@@ -140,14 +140,14 @@ int main(int argc, char **argv) {
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
const GeodesicTraceConfig trace = {.coordinate_time_step = 1, .max_steps = 2048};
const RayEndpoint ray = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
CHECK(fabs(ray.n_infinity[0] - 0.8) < 1e-12);
CHECK(fabs(ray.n_infinity[1] + 0.6) < 1e-12);
CHECK(fabs(ray.frequency_ratio - 0.8) < 1e-12);
camera.position[0] = 25; camera.position[1] = -30; camera.position[2] = 10;
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
const RayEndpoint shifted = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(shifted.status == ray.status && fabs(shifted.frequency_ratio - ray.frequency_ratio) < 1e-12);
CHECK(shifted.outcome == ray.outcome && fabs(shifted.frequency_ratio - ray.frequency_ratio) < 1e-12);
for (int i = 0; i < 3; ++i) CHECK(fabs(shifted.n_infinity[i] - ray.n_infinity[i]) < 1e-12);
camera.velocity[1] = 1;
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_NON_TIMELIKE);