Files
GR-raytracing/src/ray.c
T
wyj f7380cbf75 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.
2026-10-05 06:22:47 -04:00

236 lines
9.2 KiB
C

#include "ray.h"
#include "asymptotic.h"
#include <math.h>
#include <omp.h>
#include <stdlib.h>
int ray_pool_init(RayPool *p, size_t capacity) {
if (p == NULL || capacity == 0)
return -1;
*p = (RayPool){.capacity = capacity};
#define RAY_ALLOC(field) (p->field = calloc(capacity, sizeof *p->field))
if (!(RAY_ALLOC(t) && RAY_ALLOC(x0) && RAY_ALLOC(x1) && RAY_ALLOC(x2) &&
RAY_ALLOC(p0) && RAY_ALLOC(p1) && RAY_ALLOC(p2) && RAY_ALLOC(observer) &&
RAY_ALLOC(direction0) && RAY_ALLOC(direction1) && RAY_ALLOC(direction2) &&
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(log_alpha_p0_0) &&
RAY_ALLOC(activate_t) && RAY_ALLOC(steps) &&
RAY_ALLOC(step_limit) && RAY_ALLOC(continuation) &&
RAY_ALLOC(frame_id) && RAY_ALLOC(vertex_id) && RAY_ALLOC(status) &&
RAY_ALLOC(endpoint))) {
ray_pool_destroy(p);
return -1;
}
#undef RAY_ALLOC
return 0;
}
int ray_pool_append(RayPool *p, const ObserverState *observer,
const double direction[3],
size_t frame_id, size_t vertex_id) {
if (p == NULL || p->count == p->capacity)
return -1;
const size_t i = p->count;
if (observer == NULL || direction == NULL)
return -1;
p->t[i] = observer->coordinate_time;
p->activate_t[i] = observer->coordinate_time;
p->observer[i] = observer;
p->direction0[i] = direction[0];
p->direction1[i] = direction[1];
p->direction2[i] = direction[2];
p->frame_id[i] = frame_id;
p->vertex_id[i] = vertex_id;
p->status[i] = RAY_POOL_PENDING;
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE,
.reason = RAY_REASON_NONE,
.stop_coordinate_time = NAN,
.accepted_steps = 0,
.final_x = {NAN, NAN, NAN},
.final_Pi = {NAN, NAN, NAN},
.final_log_alpha_p0 = NAN,
.final_log_alpha_p0_0 = NAN,
.threshold_value = NAN};
p->step_limit[i] = 0;
p->continuation[i] = 0;
p->log_alpha_p0_0[i] = 0.0;
++p->count;
return 0;
}
int ray_pool_append_continuation(RayPool *p, size_t frame_id,
size_t vertex_id, double t, const double x[3],
const double Pi[3], double log_alpha_p0,
double log_alpha_p0_0, unsigned int steps,
unsigned int limit) {
if (p == NULL || p->count == p->capacity || x == NULL || Pi == NULL)
return -1;
const size_t i = p->count;
p->t[i] = t;
p->activate_t[i] = t;
p->observer[i] = NULL;
p->direction0[i] = p->direction1[i] = p->direction2[i] = 0.0;
p->x0[i] = x[0]; p->x1[i] = x[1]; p->x2[i] = x[2];
p->p0[i] = Pi[0]; p->p1[i] = Pi[1]; p->p2[i] = Pi[2];
p->log_alpha_p0[i] = log_alpha_p0;
p->log_alpha_p0_0[i] = log_alpha_p0_0;
p->steps[i] = steps;
p->step_limit[i] = limit;
p->continuation[i] = 1;
p->frame_id[i] = frame_id;
p->vertex_id[i] = vertex_id;
p->status[i] = RAY_POOL_PENDING;
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE,
.reason = RAY_REASON_NONE,
.stop_coordinate_time = NAN,
.accepted_steps = steps,
.final_x = {NAN, NAN, NAN},
.final_Pi = {NAN, NAN, NAN},
.final_log_alpha_p0 = NAN,
.final_log_alpha_p0_0 = log_alpha_p0_0,
.threshold_value = NAN};
++p->count;
return 0;
}
void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
if (p == NULL || source == NULL)
return;
#pragma omp parallel for schedule(static)
for (size_t i = 0; i < p->count; ++i) {
if (p->status[i] != RAY_POOL_PENDING || p->continuation[i])
continue;
AsymptoticRoute route;
const AsymptoticStatus status = asymptotic_route_camera(
source, p->observer[i],
(double[]){p->direction0[i], p->direction1[i], p->direction2[i]},
&route);
if (status == ASYMPTOTIC_UNSUPPORTED || status == ASYMPTOTIC_INVALID) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED
? RAY_REASON_UNSUPPORTED
: RAY_REASON_PROTOCOL_ERROR;
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_FAILED;
continue;
}
if (status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
p->endpoint[i].n_infinity[axis] = route.n_infinity[axis];
p->endpoint[i].frequency_ratio = route.frequency_ratio;
p->endpoint[i].end_id = route.end_id;
p->endpoint[i].outcome = RAY_OUTCOME_ESCAPED;
p->endpoint[i].reason = RAY_REASON_NONE;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind == ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_PROTOCOL_ERROR;
p->status[i] = RAY_POOL_FAILED;
continue;
}
p->activate_t[i] = route.activate_t;
p->x0[i] = route.x[0];
p->x1[i] = route.x[1];
p->x2[i] = route.x[2];
p->p0[i] = route.Pi[0];
p->p1[i] = route.Pi[1];
p->p2[i] = route.Pi[2];
p->log_alpha_p0[i] = route.log_alpha_p0;
/* Camera-event reference, distinct from the entry-state L. */
p->log_alpha_p0_0[i] = route.log_alpha_p0_camera;
}
}
void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) {
for (size_t i = 0; i < p->count; ++i) {
if (p->status[i] != RAY_POOL_PENDING || p->activate_t[i] > slab->t_hi ||
p->activate_t[i] <= slab->t_lo)
continue;
p->t[i] = p->activate_t[i];
/* Continuation rays keep the accepted-step count they already consumed. */
if (!p->continuation[i])
p->steps[i] = 0;
p->status[i] = RAY_POOL_ACTIVE;
}
}
void ray_pool_advance_active(RayPool *p, const MetricSlab *slab,
const GeodesicTraceConfig *config) {
/* Samples are grouped by frame, so a time slab activates only part of the
* pool. Give workers bounded chunks instead of one contiguous frame range;
* dynamic assignment also balances varying strong-field integration costs.
* Pool slots stay stable for endpoint installation after the slab sweep. */
#pragma omp parallel for schedule(dynamic, 32)
for (size_t i = 0; i < p->count; ++i) {
if (p->status[i] != RAY_POOL_ACTIVE)
continue;
GeodesicRayState s = {.coordinate_time = p->t[i],
.x = {p->x0[i], p->x1[i], p->x2[i]},
.Pi = {p->p0[i], p->p1[i], p->p2[i]},
.log_alpha_p0 = p->log_alpha_p0[i],
.log_alpha_p0_0 = p->log_alpha_p0_0[i],
.steps = p->steps[i]};
GeodesicTraceConfig per_ray = *config;
if (p->step_limit[i] != 0)
per_ray.max_steps = p->step_limit[i];
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, &s, slab->t_lo, &per_ray, &p->endpoint[i]);
p->t[i] = s.coordinate_time;
p->x0[i] = s.x[0]; p->x1[i] = s.x[1]; p->x2[i] = s.x[2];
p->p0[i] = s.Pi[0]; p->p1[i] = s.Pi[1]; p->p2[i] = s.Pi[2];
p->log_alpha_p0[i] = s.log_alpha_p0;
p->steps[i] = s.steps;
if (result == GEODESIC_ADVANCE_TERMINATED)
p->status[i] = p->endpoint[i].outcome == RAY_OUTCOME_UNRESOLVED
? RAY_POOL_UNRESOLVED
: RAY_POOL_TERMINATED;
else if (result == GEODESIC_ADVANCE_FAILED)
p->status[i] = RAY_POOL_FAILED;
}
}
int ray_pool_has_live(const RayPool *p) {
if (p == NULL)
return 0;
for (size_t i = 0; i < p->count; ++i)
if (p->status[i] == RAY_POOL_PENDING || p->status[i] == RAY_POOL_ACTIVE)
return 1;
return 0;
}
void ray_pool_destroy(RayPool *p) {
if (p == NULL)
return;
free(p->t); free(p->x0); free(p->x1); free(p->x2); free(p->observer);
free(p->direction0); free(p->direction1); free(p->direction2);
free(p->p0); free(p->p1); free(p->p2); free(p->log_alpha_p0);
free(p->log_alpha_p0_0);
free(p->activate_t); free(p->steps); free(p->step_limit);
free(p->continuation); free(p->frame_id); free(p->vertex_id);
free(p->status);
free(p->endpoint);
*p = (RayPool){0};
}