Files
GR-raytracing/tests/test_geodesic_adaptive.c
T
wyj 0a46a7095b 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.
2026-10-05 20:27:42 -04:00

2622 lines
106 KiB
C

/*
* Focused regression for the explicit DP5(4) adaptive geodesic core and its
* RayPool integration.
*
* The production stepper (src/geodesic.c), the production RHS, the analytic
* Minkowski and Schwarzschild backends, the observer builder and the
* production RayPool scheduler are exercised directly. The single-trace
* geodesic core, `geodesic_advance_past_ray`, and the batch pool's activation,
* OMP bulk advance and resume-state handling are covered here.
*
* Both analytic providers define spacetime_create_default, so they are
* textually included with a renamed provider symbol; COMMON_SOURCES excludes
* them from the link line.
*/
#define spacetime_create_default spacetime_create_default_minkowski_adaptive
#include "../src/spacetime_minkowski.c"
#undef spacetime_create_default
#define spacetime_create_default spacetime_create_default_schwarzschild_adaptive
#include "../src/spacetime_schwarzschild.c"
#undef spacetime_create_default
#include "asymptotic.h"
#include "asymptotic_schwarzschild.h"
#include "geodesic.h"
#include "observer.h"
#include "ray.h"
#include "spacetime.h"
#include <float.h>
#include <math.h>
#include <omp.h>
#include <stdio.h>
#include <string.h>
static int failures = 0;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \
++failures; \
} \
} while (0)
static double dot3(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static int invert3(const double g[3][3], double inv[3][3]) {
const double d = g[0][0] * (g[1][1] * g[2][2] - g[1][2] * g[2][1]) -
g[0][1] * (g[1][0] * g[2][2] - g[1][2] * g[2][0]) +
g[0][2] * (g[1][0] * g[2][1] - g[1][1] * g[2][0]);
if (!isfinite(d) || fabs(d) < 1e-300)
return -1;
inv[0][0] = (g[1][1] * g[2][2] - g[1][2] * g[2][1]) / d;
inv[0][1] = (g[0][2] * g[2][1] - g[0][1] * g[2][2]) / d;
inv[0][2] = (g[0][1] * g[1][2] - g[0][2] * g[1][1]) / d;
inv[1][0] = (g[1][2] * g[2][0] - g[1][0] * g[2][2]) / d;
inv[1][1] = (g[0][0] * g[2][2] - g[0][2] * g[2][0]) / d;
inv[1][2] = (g[0][2] * g[1][0] - g[0][0] * g[1][2]) / d;
inv[2][0] = (g[1][0] * g[2][1] - g[1][1] * g[2][0]) / d;
inv[2][1] = (g[0][1] * g[2][0] - g[0][0] * g[2][1]) / d;
inv[2][2] = (g[0][0] * g[1][1] - g[0][1] * g[1][0]) / d;
return 0;
}
static double null_residual(const MetricData *m, const double Pi[3]) {
double inv[3][3];
if (invert3(m->gamma, inv))
return NAN;
double value = 0.0;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
value += inv[i][j] * Pi[i] * Pi[j];
return value;
}
/* Stationary KS Killing energy reference (oracle cross-check only). */
static double killing_energy(const MetricData *m, const double Pi[3],
double log_alpha_p0) {
return exp(log_alpha_p0) * (m->alpha - dot3(m->beta, Pi));
}
static RayEndpoint blank_endpoint(void) {
RayEndpoint out;
memset(&out, 0, sizeof out);
out.magnification = 1.0;
out.end_id = SPACETIME_END_NONE;
out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_NONE;
out.threshold_value = NAN;
out.stop_coordinate_time = NAN;
return out;
}
static GeodesicTraceConfig dp_config(double tol, double initial_step,
double lookback) {
GeodesicTraceConfig c;
memset(&c, 0, sizeof c);
c.stepper = GEODESIC_STEPPER_DP54;
c.coordinate_time_step = initial_step;
c.max_steps = 1000000u;
c.threshold = (ThresholdPolicy){.kind = THRESHOLD_DISABLED,
.value = 0.0,
.policy_version = 0};
c.atol_x = tol;
c.atol_Pi = tol;
c.atol_L = tol;
c.rtol = tol;
c.min_step = 1e-12;
c.max_step = 1e6;
c.consecutive_rejection_limit = 1000u;
c.max_lookback_time = lookback;
return c;
}
static ObserverState flat_observer_at(const double p[3]) {
ObserverState o;
memset(&o, 0, sizeof o);
o.coordinate_position[0] = p[0];
o.coordinate_position[1] = p[1];
o.coordinate_position[2] = p[2];
o.tetrad[0][0] = 1.0;
o.tetrad[1][1] = 1.0;
o.tetrad[2][2] = 1.0;
o.tetrad[3][3] = 1.0;
return o;
}
static MetricData flat_metric(void) {
MetricData m;
memset(&m, 0, sizeof m);
m.alpha = 1.0;
m.gamma[0][0] = m.gamma[1][1] = m.gamma[2][2] = 1.0;
return m;
}
/* ------------------------------------------------------------------ */
/* Synthetic fixture: flat / linear-potential curved metric with a */
/* configurable spatial domain, invalid-metric and unavailable-time */
/* injection windows. Declares a single fixed Minkowski end. */
/* ------------------------------------------------------------------ */
typedef struct {
int has_domain; /* enable the [domain_lo, domain_hi] wall check */
double domain_lo, domain_hi;
SpacetimePointStatus bad_status; /* injection status (OK disables) */
double bad_lo, bad_hi;
double radius; /* escape worldtube radius */
} FixtureContext;
static SpacetimePointStatus fixture_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
const FixtureContext *c = source->context;
if (c->bad_status != SPACETIME_POINT_OK && t >= c->bad_lo && t <= c->bad_hi)
return c->bad_status;
if (c->has_domain && x[0] >= c->domain_lo && x[0] <= c->domain_hi)
return SPACETIME_POINT_OUT_OF_DOMAIN;
*metric = flat_metric();
return SPACETIME_POINT_OK;
}
static SpacetimeRayStatus fixture_classify(const SpacetimeSource *source,
double t, const double x[3]) {
(void)source;
(void)t;
(void)x;
return SPACETIME_RAY_ACTIVE;
}
static size_t fixture_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int fixture_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
(void)source;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int fixture_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const FixtureContext *c = source->context;
(void)t;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = c->radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static void fixture_destroy(SpacetimeSource *source) {
source->context = NULL;
source->ops = NULL;
}
static const SpacetimeOps fixture_ops = {
.eval = fixture_eval,
.classify = fixture_classify,
.asymptotic_end_count = fixture_end_count,
.asymptotic_end = fixture_end,
.escape_worldtube_sample = fixture_worldtube,
.destroy = fixture_destroy,
};
/* ------------------------------------------------------------------ */
/* 1. Minkowski finite interval: DP accuracy, translation, slabs, count */
/* ------------------------------------------------------------------ */
static void test_minkowski_finite_interval(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_minkowski(&source, 1.0e9) == 0, "create minkowski");
const double position[3] = {3.0, -4.0, 5.0};
const double direction[3] = {0.36, 0.48, 0.8};
const ObserverState observer = flat_observer_at(position);
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, position, &metric) == SPACETIME_POINT_OK,
"minkowski metric");
GeodesicRayState state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&state) == 0,
"minkowski init");
const GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6);
state.next_step = config.coordinate_time_step;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, "mink slab");
RayEndpoint endpoint = blank_endpoint();
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, &state, -2.0, &config, &endpoint);
CHECK(result == GEODESIC_ADVANCE_ACTIVE, "finite interval stays active");
/* Flat RHS: x(t) = x0 + (t - t0) * Pi and L constant. */
CHECK(fabs(state.coordinate_time + 2.0) < 1e-12, "reached interval end");
for (int i = 0; i < 3; ++i) {
const double expected = position[i] - 2.0 * state.Pi[i];
CHECK(fabs(state.x[i] - expected) < 1e-9, "minkowski translation exact");
}
CHECK(fabs(state.log_alpha_p0) < 1e-12, "flat L stays zero");
CHECK(state.rhs_evaluations == 7u * ((unsigned long)state.steps +
(unsigned long)state.rejected_steps),
"actual RHS count equals full DP trials");
CHECK(state.steps > 0, "accepted at least one step");
spacetime_free_slab(slab);
/* Translation independence: the same boost shifted by a constant vector
* produces the same state shifted by that vector. */
{
const double shift[3] = {100.0, -200.0, 300.0};
double shifted[3];
for (int i = 0; i < 3; ++i)
shifted[i] = position[i] + shift[i];
const ObserverState observer2 = flat_observer_at(shifted);
MetricData metric2;
CHECK(spacetime_eval(&source, 0.0, shifted, &metric2) == SPACETIME_POINT_OK,
"shifted metric");
GeodesicRayState state2;
CHECK(geodesic_initialize_past_ray_metric(&metric2, &observer2, direction,
&state2) == 0,
"shifted init");
state2.next_step = config.coordinate_time_step;
MetricSlab *slab2 = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab2) == 0,
"shifted slab");
RayEndpoint endpoint2 = blank_endpoint();
(void)geodesic_advance_past_ray(slab2, &state2, -2.0, &config, &endpoint2);
for (int i = 0; i < 3; ++i)
CHECK(fabs((state2.x[i] - shift[i]) - state.x[i]) < 1e-9,
"translation invariance");
spacetime_free_slab(slab2);
}
/* Cross-slab continuity: two partial slabs must match one long slab. */
{
GeodesicRayState split;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&split) == 0,
"split init");
split.next_step = config.coordinate_time_step;
MetricSlab *first = NULL, *second = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.5, &first) == 0, "first slab");
RayEndpoint e1 = blank_endpoint();
const GeodesicAdvanceResult r1 =
geodesic_advance_past_ray(first, &split, -1.0, &config, &e1);
spacetime_free_slab(first);
CHECK(r1 == GEODESIC_ADVANCE_ACTIVE, "first partial slab active");
CHECK(spacetime_load_slab(&source, split.coordinate_time, -3.0, &second) ==
0,
"second slab");
RayEndpoint e2 = blank_endpoint();
const GeodesicAdvanceResult r2 =
geodesic_advance_past_ray(second, &split, -2.0, &config, &e2);
spacetime_free_slab(second);
CHECK(r2 == GEODESIC_ADVANCE_ACTIVE, "second partial slab active");
for (int i = 0; i < 3; ++i)
CHECK(fabs(split.x[i] - state.x[i]) < 1e-7, "cross-slab continuity x");
CHECK(fabs(split.log_alpha_p0 - state.log_alpha_p0) < 1e-9,
"cross-slab continuity L");
}
/* A tiny slab-left interval must still accept a boundary-limited step even
* below min_step, without permanently depressing the next suggestion. */
{
GeodesicRayState tiny;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&tiny) == 0,
"tiny init");
GeodesicTraceConfig tiny_config = dp_config(1e-9, 1.0, 1.0e6);
tiny_config.min_step = 0.25;
tiny.next_step = tiny_config.coordinate_time_step;
MetricSlab *s = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -0.1, &s) == 0, "tiny slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(s, &tiny, -1e-6, &tiny_config, &out);
spacetime_free_slab(s);
CHECK(r == GEODESIC_ADVANCE_ACTIVE, "tiny boundary step active");
CHECK(fabs(tiny.coordinate_time + 1e-6) < 1e-18, "tiny boundary reached");
CHECK(tiny.next_step == tiny_config.coordinate_time_step,
"tiny boundary kept the original proposal");
}
/* Invalid DP configuration must be rejected explicitly, not defaulted. */
{
GeodesicRayState bad_state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&bad_state) == 0,
"bad config init");
MetricSlab *s = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &s) == 0, "bad slab");
GeodesicTraceConfig bad = dp_config(1e-9, 0.5, 1.0e6);
bad.atol_x = 0.0;
RayEndpoint out = blank_endpoint();
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"zero atol rejected");
bad = dp_config(1e-9, 0.5, 1.0e6);
bad.max_lookback_time = 0.0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"zero lookback rejected");
bad = dp_config(1e-9, 0.5, 1.0e6);
bad.min_step = 2.0;
bad.max_step = 1.0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"inverted step bounds rejected");
bad = dp_config(1e-9, 0.5, 1.0e6);
bad.consecutive_rejection_limit = 0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"zero reject limit rejected");
spacetime_free_slab(s);
}
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 2. Schwarzschild radial branches, interior observer, residuals */
/* ------------------------------------------------------------------ */
static void radial_static_observer(const MetricData *metric, double r0,
ObserverState *out) {
*out = (ObserverState){.coordinate_time = 0.0,
.coordinate_position = {r0, 0.0, 0.0}};
const double alpha = metric->alpha;
out->tetrad[0][0] = 1.0 / alpha;
for (int i = 0; i < 3; ++i)
out->tetrad[0][i + 1] = -metric->beta[i] / alpha;
out->tetrad[1][1] = 1.0 / sqrt(metric->gamma[0][0]);
out->tetrad[2][2] = 1.0;
out->tetrad[3][3] = 1.0;
}
static int trace_radial_dp(const SpacetimeSource *source, const ObserverState *o,
double direction, double duration,
const GeodesicTraceConfig *config,
GeodesicRayState *state) {
MetricData metric;
if (spacetime_eval(source, o->coordinate_time, o->coordinate_position,
&metric) != SPACETIME_POINT_OK)
return -1;
const double n[3] = {direction, 0.0, 0.0};
if (geodesic_initialize_past_ray_metric(&metric, o, n, state))
return -1;
state->next_step = config->coordinate_time_step;
MetricSlab *slab = NULL;
if (spacetime_load_slab(source, 0.0, -duration - 1.0, &slab))
return -1;
RayEndpoint endpoint = blank_endpoint();
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, state, -duration, config, &endpoint);
spacetime_free_slab(slab);
return result == GEODESIC_ADVANCE_FAILED ? -1 : 0;
}
static void test_schwarzschild_radial(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
const double r0 = 10.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) ==
SPACETIME_POINT_OK,
"r0 metric");
ObserverState observer;
radial_static_observer(&metric, r0, &observer);
const GeodesicTraceConfig config = dp_config(1e-7, 0.5, 1.0e6);
GeodesicRayState outward;
CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &config, &outward) == 0,
"outward trace");
const double s_out = -outward.coordinate_time;
CHECK(fabs(outward.x[0] - r0 - s_out) < 1e-4,
"outward closed-form invariant");
MetricData out_metric;
CHECK(spacetime_eval(&source, outward.coordinate_time, outward.x,
&out_metric) == SPACETIME_POINT_OK,
"outward end metric");
CHECK(fabs(null_residual(&out_metric, outward.Pi) - 1.0) < 1e-6,
"outward null constraint");
GeodesicRayState inward;
CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &config, &inward) == 0,
"inward trace");
const double s_in = -inward.coordinate_time;
const double c0 = (r0 - 2.0) + 4.0 * log(r0 - 2.0);
const double c1 = (inward.x[0] - 2.0) + 4.0 * log(inward.x[0] - 2.0) + s_in;
CHECK(inward.x[0] > 2.0, "inward stays outside horizon");
CHECK(inward.x[0] < r0, "inward decreases r");
CHECK(fabs(c1 - c0) < 1e-4, "inward closed-form invariant");
MetricData in_metric;
CHECK(spacetime_eval(&source, inward.coordinate_time, inward.x, &in_metric) ==
SPACETIME_POINT_OK,
"inward end metric");
CHECK(fabs(null_residual(&in_metric, inward.Pi) - 1.0) < 1e-6,
"inward null constraint");
/* Killing energy conservation along a radial ray. */
{
GeodesicRayState start;
MetricData m0 = metric;
CHECK(geodesic_initialize_past_ray_metric(
&m0, &observer, (double[]){1.0, 0.0, 0.0}, &start) == 0,
"killing init");
GeodesicRayState end;
CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &config, &end) == 0,
"killing trace");
MetricData m1;
CHECK(spacetime_eval(&source, end.coordinate_time, end.x, &m1) ==
SPACETIME_POINT_OK,
"killing end metric");
const double ek_start = killing_energy(&m0, start.Pi, start.log_alpha_p0);
const double ek_end = killing_energy(&m1, end.Pi, end.log_alpha_p0);
CHECK(fabs(ek_end / ek_start - 1.0) < 1e-5,
"Killing energy conserved along DP ray");
}
/* Interior r = 1.5 free-fall-from-rest-at-infinity observer: legal
* timelike observer, null constraint along the integrated ray. */
{
const double r = 1.5;
const double f = 1.0 - 2.0 / r;
const double ur = -sqrt(2.0 / r);
const double uks = 1.0 / f + (2.0 / (r - 2.0)) * ur;
const double vx = ur / uks;
ObserverCamera camera = {.position = {r, 0.0, 0.0},
.velocity = {vx, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0,
.roll_deg = 0.0};
MetricData m;
ObserverState inner;
CHECK(spacetime_eval(&source, 0.0, camera.position, &m) ==
SPACETIME_POINT_OK &&
observer_from_coordinate_camera(&m, &camera, &inner, NULL) ==
OBSERVER_BUILD_OK,
"interior free-fall observer");
GeodesicRayState state;
CHECK(trace_radial_dp(&source, &inner, 1.0, 1.0, &config, &state) == 0,
"interior trace");
MetricData end_metric;
CHECK(spacetime_eval(&source, state.coordinate_time, state.x, &end_metric) ==
SPACETIME_POINT_OK,
"interior end metric");
CHECK(fabs(null_residual(&end_metric, state.Pi) - 1.0) < 1e-5,
"interior null constraint");
}
/* Tolerance tightening must improve (or preserve) the radial residual. */
{
const GeodesicTraceConfig loose = dp_config(1e-5, 0.5, 1.0e6);
const GeodesicTraceConfig tight = dp_config(1e-9, 0.5, 1.0e6);
GeodesicRayState sl, st;
CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &loose, &sl) == 0 &&
trace_radial_dp(&source, &observer, 1.0, 2.0, &tight, &st) == 0,
"tolerance scan traces");
const double slow = fabs(sl.x[0] - r0 + sl.coordinate_time);
const double stight = fabs(st.x[0] - r0 + st.coordinate_time);
CHECK(stight <= slow + 1e-9, "tighter tolerance no worse residual");
CHECK(stight < 1e-6, "tight residual small");
}
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 3. Over-large initial step: rejection without accepted-state damage */
/* ------------------------------------------------------------------ */
static void test_over_large_initial_step(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
const double r0 = 10.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) ==
SPACETIME_POINT_OK,
"metric");
ObserverState observer;
radial_static_observer(&metric, r0, &observer);
const GeodesicTraceConfig small = dp_config(1e-9, 0.05, 1.0e6);
GeodesicRayState reference;
CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &small, &reference) == 0,
"reference small-step trace");
GeodesicTraceConfig big = dp_config(1e-9, 4.0, 1.0e6);
GeodesicRayState large;
CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &big, &large) == 0,
"large initial step trace");
CHECK(large.rejected_steps > 0, "large initial step was rejected");
/* Comparing final states is the available evidence that rejected trials did
* not pollute the accepted trajectory; it is not a direct internal proof. */
CHECK(fabs(large.coordinate_time - reference.coordinate_time) < 1e-12,
"large-step trace reaches the same time");
for (int i = 0; i < 3; ++i)
CHECK(fabs(large.x[i] - reference.x[i]) < 1e-4,
"large-step result matches small-step reference");
CHECK(fabs(large.log_alpha_p0 - reference.log_alpha_p0) < 1e-5,
"large-step L matches reference");
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 4. Synthetic fixture: retryable stages, fatal points, hmin/maxreject */
/* ------------------------------------------------------------------ */
/* Build a manual flat state at x = (x0, 0, 0) moving in +x in the past
* (Pi = -xhat), used by the fixture tests. */
static void fixture_state(double x0, GeodesicRayState *state) {
memset(state, 0, sizeof *state);
state->x[0] = x0;
state->Pi[0] = -1.0;
}
static void test_fixture_stage_retry(void) {
/* Step-size-dependent retryable stage failure: a thin forbidden wall lies
* across the outward path. The initial trial places a stage inside the
* wall (OUT_OF_DOMAIN), is rejected and shrunk; a smaller/reshaped step then
* jumps over the wall and the integration completes. */
FixtureContext context;
memset(&context, 0, sizeof context);
context.has_domain = 1;
context.domain_lo = 0.9;
context.domain_hi = 0.95;
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-8, 1.15, 1.0e6);
state.next_step = config.coordinate_time_step;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, "wall slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, &state, -2.0, &config, &out);
spacetime_free_slab(slab);
CHECK(result == GEODESIC_ADVANCE_ACTIVE, "wall overshoot eventually active");
CHECK(fabs(state.coordinate_time + 2.0) < 1e-9, "wall reached target");
CHECK(state.rejected_steps > 0, "wall overshoot caused a rejection");
CHECK(fabs(state.x[0] - 2.0) < 1e-9, "wall integration stayed on the ray");
}
static void test_fixture_fatal_and_bounds(void) {
/* The initial accepted state itself is invalid: direct specific failure,
* one RHS call, no shrink. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_INVALID_METRIC;
context.bad_lo = -1.0e30;
context.bad_hi = 1.0e30;
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "invalid slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -0.5, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_INVALID_METRIC,
"initial invalid metric is a direct specific failure");
CHECK(state.rhs_evaluations == 1u, "initial failure used exactly one RHS");
CHECK(state.rejected_steps == 0u, "initial failure did not reject-retry");
}
/* A later stage hits TIME_UNAVAILABLE: direct specific failure, no shrink. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE;
context.bad_lo = -0.3;
context.bad_hi = -0.01;
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "unavail slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -0.5, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"stage TIME_UNAVAILABLE is a direct specific failure");
CHECK(state.rhs_evaluations == 2u,
"stage failure was not retried with repeated shrink");
CHECK(state.rejected_steps == 0u, "stage TIME_UNAVAILABLE not a reject");
}
/* A later stage hits INTERNAL_ERROR: direct protocol failure. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_INTERNAL_ERROR;
context.bad_lo = -0.3;
context.bad_hi = -0.01;
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "internal slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -0.5, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"stage INTERNAL_ERROR is a direct protocol failure");
CHECK(state.rhs_evaluations == 2u, "internal error not retried");
}
/* Retryable stage OUT_OF_DOMAIN that never clears: hmin and maxreject. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN;
context.bad_lo = -1.0e30;
context.bad_hi = -0.001; /* stage 1 (t = -h/5) is always in the window */
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig hmin = dp_config(1e-8, 1.0, 1.0e6);
hmin.min_step = 0.1;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0, "hmin slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -10.0, &hmin, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_INTEGRATION_ERROR,
"step below hmin is an integration error");
CHECK(state.rejected_steps >= 1u, "hmin path rejected before failing");
FixtureContext context2;
memset(&context2, 0, sizeof context2);
context2.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN;
context2.bad_lo = -1.0e30;
context2.bad_hi = -0.001;
context2.radius = 1.0e9;
SpacetimeSource source2 = {.ops = &fixture_ops, .context = &context2};
GeodesicRayState state2;
fixture_state(0.0, &state2);
GeodesicTraceConfig maxreject = dp_config(1e-8, 1.0, 1.0e6);
maxreject.min_step = 1e-12;
maxreject.consecutive_rejection_limit = 2;
MetricSlab *slab2 = NULL;
CHECK(spacetime_load_slab(&source2, 0.0, -10.0, &slab2) == 0,
"maxreject slab");
RayEndpoint out2 = blank_endpoint();
const GeodesicAdvanceResult r2 =
geodesic_advance_past_ray(slab2, &state2, -10.0, &maxreject, &out2);
spacetime_free_slab(slab2);
CHECK(r2 == GEODESIC_ADVANCE_FAILED &&
out2.reason == RAY_REASON_OUT_OF_DOMAIN,
"consecutive rejection limit reports the specific stage reason");
CHECK(state2.rejected_steps == 2u, "maxreject counted exactly two rejects");
CHECK(state2.coordinate_time == 0.0,
"rejected trials did not advance the accepted time");
}
}
/* ------------------------------------------------------------------ */
/* 5. Budget, explicit lookback, history exhaustion, resume */
/* ------------------------------------------------------------------ */
static GeodesicRayState continuation_from(const RayEndpoint *e) {
GeodesicRayState s;
memset(&s, 0, sizeof s);
s.coordinate_time = e->stop_coordinate_time;
for (int i = 0; i < 3; ++i) {
s.x[i] = e->final_x[i];
s.Pi[i] = e->final_Pi[i];
}
s.log_alpha_p0 = e->final_log_alpha_p0;
s.log_alpha_p0_0 = e->final_log_alpha_p0_0;
s.steps = e->accepted_steps;
s.integration_start_time = e->integration_start_time;
s.next_step = e->next_step;
s.rejected_steps = e->rejected_steps;
s.rhs_evaluations = e->rhs_evaluations;
s.previous_rejected = e->previous_rejected;
return s;
}
static void test_budget_lookback_and_resume(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski");
const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0});
const double direction[3] = {1.0, 0.0, 0.0};
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){0.0, 0.0, 0.0}, &metric) ==
SPACETIME_POINT_OK,
"metric");
/* Accepted-step budget exhaustion -> UNRESOLVED/BUDGET_EXHAUSTED. */
{
GeodesicRayState state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&state) == 0,
"budget init");
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6);
config.max_steps = 3;
/* Keep three accepted steps well inside the r = 10 escape sphere. */
config.max_step = 1.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -50.0, &slab) == 0, "budget slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -50.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_UNRESOLVED &&
out.reason == RAY_REASON_BUDGET_EXHAUSTED,
"max_steps yields UNRESOLVED/BUDGET_EXHAUSTED");
CHECK(state.steps == 3u, "budget consumed exactly max_steps");
}
/* Explicit lookback budget exhaustion -> UNRESOLVED, not history. */
{
GeodesicRayState state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&state) == 0,
"lookback init");
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 0.25);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -50.0, &slab) == 0,
"lookback slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -50.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_UNRESOLVED &&
out.reason == RAY_REASON_BUDGET_EXHAUSTED,
"explicit lookback yields UNRESOLVED/BUDGET_EXHAUSTED");
CHECK(fabs(state.coordinate_time + 0.25) < 1e-9,
"lookback stopped at the explicit budget");
}
/* A source slab data time hole is TIME_RANGE_EXHAUSTED, not UNRESOLVED. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE;
context.bad_lo = -1.0e30;
context.bad_hi = -0.3;
context.radius = 1.0e9;
SpacetimeSource fs = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config = dp_config(1e-9, 0.25, 1.0e6);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&fs, 0.0, -1.0, &slab) == 0, "hole slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"slab data hole is a specific INCOMPLETE reason, not UNRESOLVED");
}
/* Resume preserves L0, start time, counters and next step; an explicitly
* larger lookback budget continues the trace to escape without replay. */
{
GeodesicTraceConfig first = dp_config(1e-9, 0.5, 1.0);
const RayEndpoint partial =
geodesic_trace_past(&source, &observer, direction, &first);
CHECK(partial.outcome == RAY_OUTCOME_UNRESOLVED &&
partial.reason == RAY_REASON_BUDGET_EXHAUSTED,
"first lookback trace is unresolved");
const double l0 = partial.final_log_alpha_p0_0;
const double start = partial.integration_start_time;
const unsigned long rhs_before = partial.rhs_evaluations;
GeodesicRayState continuation = continuation_from(&partial);
GeodesicTraceConfig retry = dp_config(1e-9, 0.5, 100.0);
const RayEndpoint resumed =
geodesic_trace_past_from_state(&source, &continuation, &retry);
CHECK(resumed.outcome == RAY_OUTCOME_ESCAPED, "resumed trace escapes");
CHECK(resumed.final_log_alpha_p0_0 == l0, "resume preserves L0");
CHECK(resumed.integration_start_time == start,
"resume preserves integration start time");
CHECK(resumed.rhs_evaluations > rhs_before,
"resume accumulates RHS cost without replaying from scratch");
CHECK(resumed.accepted_steps >= partial.accepted_steps,
"resume keeps accepted step count");
}
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 6. Escape event localization: flat analytic + strong-field compare */
/* ------------------------------------------------------------------ */
static void test_flat_escape_localization(void) {
FixtureContext context;
memset(&context, 0, sizeof context);
context.has_domain = 0;
context.radius = 5.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
/* Start inside the worldtube and integrate outward so the crossing is
* produced by the advance loop itself, not by the pre-route. */
GeodesicRayState state;
fixture_state(2.0, &state);
GeodesicTraceConfig config = dp_config(1e-10, 0.5, 1.0e6);
state.next_step = config.coordinate_time_step;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, "escape slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -20.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED && out.end_id == 0,
"flat DP escape detected by the advance loop");
CHECK(fabs(out.n_infinity[0] - 1.0) < 1e-6 &&
fabs(out.n_infinity[1]) < 1e-6 && fabs(out.n_infinity[2]) < 1e-6,
"flat DP escape direction");
const double expected_crossing = -(context.radius - 2.0);
CHECK(fabs(out.stop_coordinate_time - expected_crossing) < 1e-4,
"flat DP crossing time matches analytic root");
}
/* Event localization with a translated time origin. At t0 = 1e12 the local
* coordinate-time ULP is ~1.2e-4, so the crossing cannot be resolved below
* that; the old relative epsilon (= 1.0 at 1e12) skipped the subintegration
* and fabricated an off-trajectory endpoint. The endpoint must stay on the
* flat ray x(t) = 2 + (t0 - t) to a few ULP. */
static void check_flat_crossing_at(double t0, double position_tol,
double time_tol) {
FixtureContext context;
memset(&context, 0, sizeof context);
context.radius = 5.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(2.0, &state);
state.coordinate_time = t0;
state.integration_start_time = t0;
GeodesicTraceConfig config = dp_config(1e-10, 0.5, 100.0);
config.max_step = 0.5;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, t0, t0 - 20.0, &slab) == 0,
"translated slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, t0 - 20.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"translated flat escape");
CHECK(fabs(out.final_x[0] - 5.0) <= position_tol,
"translated crossing position matches analytic root");
CHECK(fabs((t0 - out.stop_coordinate_time) - 3.0) <= time_tol,
"translated crossing time matches analytic root");
CHECK(fabs(out.final_x[0] - 2.0 - (t0 - out.stop_coordinate_time)) <=
2.0 * position_tol,
"translated endpoint lies on the flat ray");
}
static void test_event_time_translation(void) {
check_flat_crossing_at(0.0, 1e-6, 1e-6);
check_flat_crossing_at(1e12, 2e-3, 2e-3);
}
/* A TIME_UNAVAILABLE window that the accepted main trial does not hit but a
* bisection subintegration does must surface as INCOMPLETE /
* TIME_RANGE_EXHAUSTED (the metric-integration reason), not as a worldtube
* protocol error, with the last trusted state restored and the failed
* subintegration RHS calls accumulated. */
static void test_event_subintegration_time_hole(void) {
FixtureContext context;
memset(&context, 0, sizeof context);
context.radius = 2.7;
context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE;
/* The dense escape bracket re-localizes on the accepted trajectory; the
* bisection reaches t = -0.595 here, so the injected window must cover it.
* Only the subintegration sees the hole, not the accepted main trial. */
context.bad_lo = -0.596;
context.bad_hi = -0.594;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(2.0, &state);
GeodesicTraceConfig config = dp_config(1e-10, 1.0, 100.0);
config.max_step = 1.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -2.0, &slab) == 0,
"event hole slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -2.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE,
"event subintegration hole fails as incomplete");
CHECK(out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"event subintegration propagates TIME_RANGE_EXHAUSTED");
CHECK(out.reason != RAY_REASON_PROTOCOL_ERROR,
"event integration reason is not a worldtube protocol error");
CHECK(state.coordinate_time == 0.0 && out.stop_coordinate_time == 0.0,
"event hole restores the last trusted time");
CHECK(out.final_x[0] == 2.0,
"event hole restores the last trusted position");
CHECK(state.rhs_evaluations > 7u,
"event hole RHS includes the failed subintegration calls");
}
static void test_schwarzschild_dp_escape_matches_finish(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
SchwarzschildCanonical canonical = {.end_id = 0,
.t = 0.0,
.rho = 256.0,
.rhat = {0.8, 0.6, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = 5.0,
.energy = 1.0,
.radial_sign = 1};
double x[3], Pi[3], log_alpha_p0;
CHECK(asymptotic_schwarzschild_state_from_canonical(
&end, &canonical, x, Pi, &log_alpha_p0) == 0,
"canonical state");
double n_analytic[3], freq_analytic;
CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_analytic,
&freq_analytic) == 0,
"analytic finish");
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.x[0] = x[0];
state.x[1] = x[1];
state.x[2] = x[2];
state.Pi[0] = Pi[0];
state.Pi[1] = Pi[1];
state.Pi[2] = Pi[2];
state.log_alpha_p0 = log_alpha_p0;
state.log_alpha_p0_0 = log_alpha_p0;
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6);
state.next_step = config.coordinate_time_step;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0e6, &slab) == 0, "far slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0e6, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"strong-field DP integration escapes");
const double angle = acos(fmax(
-1.0, fmin(1.0, dot3(n_analytic, out.n_infinity))));
CHECK(angle < 1e-3, "DP escape direction matches analytic finish");
CHECK(fabs(out.frequency_ratio / freq_analytic - 1.0) < 1e-3,
"DP escape frequency matches analytic finish");
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 7. Legacy RK4 selection is unchanged (zero-initialized config) */
/* ------------------------------------------------------------------ */
static void test_rk4_compatibility(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski");
const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0});
GeodesicTraceConfig config;
memset(&config, 0, sizeof config);
config.coordinate_time_step = 0.25;
config.max_steps = 100;
config.threshold.kind = THRESHOLD_DISABLED;
CHECK(config.stepper == GEODESIC_STEPPER_RK4,
"zero-initialized config selects RK4");
const RayEndpoint out =
geodesic_trace_past(&source, &observer, (double[]){1.0, 0.0, 0.0},
&config);
CHECK(out.outcome == RAY_OUTCOME_ESCAPED, "RK4 escape still works");
CHECK(fabs(out.frequency_ratio - 1.0) < 1e-12, "RK4 flat frequency");
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 8. RayPool batch scheduler: OMP determinism, single-trace agreement, */
/* activation/control state and cross-slab reject accounting */
/* ------------------------------------------------------------------ */
/* Sweep a pool from `top` downward until every ray terminates or the bottom
* bound is reached, activating pending rays in each slab. */
static int pool_sweep(RayPool *pool, const SpacetimeSource *source,
const GeodesicTraceConfig *config, double top,
double bottom, double slab_duration) {
ray_pool_preroute(pool, source);
double slab_hi = top;
while (ray_pool_has_live(pool) && slab_hi > bottom) {
const double slab_lo = fmax(slab_hi - slab_duration, bottom);
MetricSlab *slab = NULL;
if (spacetime_load_slab(source, slab_hi, slab_lo, &slab))
return -1;
ray_pool_activate_in_time_range(pool, slab);
ray_pool_advance_active(pool, slab, config);
spacetime_free_slab(slab);
slab_hi = slab_lo;
}
return 0;
}
static void test_ray_pool_batch_matches_single(void) {
FixtureContext context;
memset(&context, 0, sizeof context);
context.radius = 50.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
const ObserverState observer = flat_observer_at((double[]){2.0, 0.0, 0.0});
const double camera_directions[4][3] = {{1.0, 0.0, 0.0},
{-1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}};
const GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6);
RayEndpoint by_threads[2][4];
for (int threads = 1; threads <= 2; ++threads) {
omp_set_num_threads(threads);
RayPool pool;
CHECK(ray_pool_init(&pool, 4) == 0, "batch pool init");
for (int i = 0; i < 4; ++i)
CHECK(ray_pool_append(&pool, &observer, camera_directions[i], 0,
(size_t)i) == 0,
"batch pool append");
ray_pool_preroute(&pool, &source);
for (int i = 0; i < 4; ++i) {
CHECK(pool.continuation[i] == 0, "new pool ray is not a continuation");
CHECK(pool.observer[i] == &observer, "preroute keeps the observer");
CHECK(pool.integration_start_time[i] == pool.activate_t[i],
"adaptive window starts at the activation time");
CHECK(pool.next_step[i] == 0.0,
"first trial step is deferred to the trace config");
CHECK(pool.rejected_steps[i] == 0 && pool.rhs_evaluations[i] == 0,
"new pool ray starts with zero adaptive cost");
}
CHECK(pool_sweep(&pool, &source, &config, 0.0, -1000.0, 10.0) == 0,
"batch pool sweep");
for (int i = 0; i < 4; ++i) {
CHECK(pool.status[i] == RAY_POOL_TERMINATED, "batch ray terminated");
CHECK(pool.endpoint[i].outcome == RAY_OUTCOME_ESCAPED,
"batch ray escapes");
CHECK(pool.steps[i] > 0, "batch ray accepted steps");
by_threads[threads - 1][i] = pool.endpoint[i];
}
ray_pool_destroy(&pool);
}
omp_set_num_threads(1);
for (int i = 0; i < 4; ++i) {
for (int axis = 0; axis < 3; ++axis)
CHECK(fabs(by_threads[0][i].n_infinity[axis] -
by_threads[1][i].n_infinity[axis]) < 1e-12,
"OMP1/OMP2 escape direction identical");
CHECK(fabs(by_threads[0][i].stop_coordinate_time -
by_threads[1][i].stop_coordinate_time) < 1e-12,
"OMP1/OMP2 crossing time identical");
}
for (int i = 0; i < 4; ++i) {
const RayEndpoint single =
geodesic_trace_past(&source, &observer, camera_directions[i], &config);
CHECK(single.outcome == RAY_OUTCOME_ESCAPED, "single trace escapes");
for (int axis = 0; axis < 3; ++axis)
CHECK(fabs(single.n_infinity[axis] - by_threads[0][i].n_infinity[axis]) <
1e-3,
"pool matches single-trace escape direction");
CHECK(fabs(single.stop_coordinate_time -
by_threads[0][i].stop_coordinate_time) < 1e-2,
"pool matches single-trace crossing time");
}
}
static void test_ray_pool_continuation_state(void) {
FixtureContext context;
memset(&context, 0, sizeof context);
context.radius = 1.0e9; /* no escape inside the traced window */
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0});
GeodesicTraceConfig first = dp_config(1e-9, 0.5, 1.0e6);
first.max_steps = 3;
first.max_step = 1.0;
const RayEndpoint partial = geodesic_trace_past(
&source, &observer, (double[]){-1.0, 0.0, 0.0}, &first);
CHECK(partial.outcome == RAY_OUTCOME_UNRESOLVED &&
partial.reason == RAY_REASON_BUDGET_EXHAUSTED,
"partial trace is budget-unresolved");
GeodesicRayState state = continuation_from(&partial);
const double l0 = state.log_alpha_p0_0;
const double start = state.integration_start_time;
const double t_before = state.coordinate_time;
const unsigned long rhs_before = state.rhs_evaluations;
RayPool pool;
CHECK(ray_pool_init(&pool, 1) == 0, "continuation pool init");
CHECK(ray_pool_append_continuation_state(&pool, 0, 0, &state, 6,
partial.lookback_limit) == 0,
"append continuation state");
CHECK(pool.continuation[0] == 1, "continuation is flagged");
CHECK(pool.observer[0] == NULL, "continuation carries no camera observer");
CHECK(pool.integration_start_time[0] == start &&
pool.log_alpha_p0_0[0] == l0 &&
pool.rejected_steps[0] == state.rejected_steps &&
pool.rhs_evaluations[0] == rhs_before &&
pool.next_step[0] == state.next_step,
"continuation copies the full adaptive state");
ray_pool_preroute(&pool, &source);
CHECK(pool.status[0] == RAY_POOL_PENDING && pool.observer[0] == NULL,
"continuation skips preroute");
CHECK(pool.integration_start_time[0] == start,
"preroute does not touch continuation control state");
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0,
"continuation slab");
ray_pool_activate_in_time_range(&pool, slab);
CHECK(pool.status[0] == RAY_POOL_ACTIVE && pool.steps[0] == state.steps,
"activation preserves the continuation step count");
ray_pool_advance_active(&pool, slab, &first);
spacetime_free_slab(slab);
CHECK(pool.steps[0] == 6u, "continuation reached the new step budget");
CHECK(pool.endpoint[0].outcome == RAY_OUTCOME_UNRESOLVED,
"continuation is still budget-unresolved");
CHECK(pool.t[0] < t_before, "continuation moved further into the past");
CHECK(pool.integration_start_time[0] == start &&
pool.log_alpha_p0_0[0] == l0,
"continuation preserves window start and L0");
CHECK(pool.rhs_evaluations[0] > rhs_before,
"continuation accumulates RHS without replay");
CHECK(pool.rhs_evaluations[0] ==
7ul * ((unsigned long)pool.steps[0] +
(unsigned long)pool.rejected_steps[0]),
"pool RHS count matches full DP trials across the resume");
ray_pool_destroy(&pool);
}
static void test_ray_pool_cross_slab_reject(void) {
/* A thin forbidden wall lies across the outward path: the first trial is
* rejected and shrunk. Splitting the sweep across slabs verifies that the
* accumulated rejection/RHS cost and the accepted state survive both the
* slab boundary and the pool gather/scatter. */
FixtureContext context;
memset(&context, 0, sizeof context);
context.has_domain = 1;
context.domain_lo = 0.9;
context.domain_hi = 0.95;
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0});
RayPool pool;
CHECK(ray_pool_init(&pool, 1) == 0, "reject pool init");
/* A flat camera direction n gives Pi = -n, so n = +xhat sends the past ray
* toward increasing x, across the forbidden wall. */
CHECK(ray_pool_append(&pool, &observer, (double[]){1.0, 0.0, 0.0}, 0, 0) ==
0,
"reject pool append");
const GeodesicTraceConfig config = dp_config(1e-8, 1.15, 1.0e6);
ray_pool_preroute(&pool, &source);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.5, &slab) == 0, "reject slab 1");
ray_pool_activate_in_time_range(&pool, slab);
ray_pool_advance_active(&pool, slab, &config);
spacetime_free_slab(slab);
const unsigned int rejected_first = pool.rejected_steps[0];
const unsigned long rhs_first = pool.rhs_evaluations[0];
CHECK(rejected_first > 0, "wall overshoot rejected in the first slab");
CHECK(rhs_first > 0, "first slab spent RHS calls");
CHECK(spacetime_load_slab(&source, -1.5, -2.0, &slab) == 0, "reject slab 2");
ray_pool_activate_in_time_range(&pool, slab);
ray_pool_advance_active(&pool, slab, &config);
spacetime_free_slab(slab);
CHECK(fabs(pool.t[0] + 2.0) < 1e-9, "reject pool reached the target time");
CHECK(fabs(pool.x0[0] - 2.0) < 1e-9, "reject pool stayed on the ray");
CHECK(pool.steps[0] > 0, "reject run accepted steps");
CHECK(pool.rejected_steps[0] >= rejected_first,
"rejection count survives the slab boundary");
CHECK(pool.rhs_evaluations[0] > rhs_first,
"RHS cost accumulates across slabs");
/* Every accepted trial costs 7 RHS; a rejected trial costs at least the
* initial evaluation plus the failing stage. */
CHECK(pool.rhs_evaluations[0] >=
7ul * (unsigned long)pool.steps[0] +
2ul * (unsigned long)pool.rejected_steps[0],
"cross-slab reject/RHS accounting is consistent");
ray_pool_destroy(&pool);
}
/* ------------------------------------------------------------------ */
/* 9. Dense event layer: polynomial roots, multi-end order, segments, */
/* endpoint/tangent roots, and threshold-versus-escape ordering. */
/* ------------------------------------------------------------------ */
/* Focused hook into the production dense-event polynomial root finder. */
extern int geodesic_event_polynomial_exit(const double *coeff, int degree,
double *theta, double *lo,
double *hi);
extern int geodesic_event_worldtube_dense_probe(const MetricSlab *slab,
const GeodesicTraceConfig *config,
SpacetimeEndId end_id,
GeodesicRayState *state,
double left, double theta,
double *dense_value,
double *actual_value);
static void test_event_polynomial_roots(void) {
double theta, lo, hi;
/* inside -> outside -> inside: the earliest exit at the FIRST root. The
* production root isolator must not return the later re-entry. */
{
const double c[3] = {-0.1875, 1.0, -1.0}; /* -theta^2+theta-0.1875 */
CHECK(geodesic_event_polynomial_exit(c, 2, &theta, &lo, &hi) == 1,
"in-out-in polynomial has an exit");
CHECK(fabs(theta - 0.25) < 1e-9, "earliest exit is the first root");
CHECK(lo <= 1e-12 && hi > 0.25 && hi < 0.75,
"exit bracket stops before the re-entry");
}
/* Tangent (even multiplicity, no sign change) is not an escape. */
{
const double c[3] = {-0.25, 1.0, -1.0}; /* -(theta-0.5)^2 */
CHECK(geodesic_event_polynomial_exit(c, 2, &theta, &lo, &hi) == 0,
"tangent touch is not an escape");
}
/* A root exactly at the step endpoint is left for the following step. */
{
const double c[2] = {-1.0, 1.0}; /* theta - 1 */
CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 0,
"root at theta == 1 is not an in-step exit");
}
/* A boundary start that immediately moves outside is an exit at theta 0. */
{
const double c[2] = {0.0, 1.0}; /* theta */
CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 1,
"boundary start moving outside is an exit");
CHECK(theta <= 1e-12, "boundary exit is at theta 0");
}
/* outside -> inside (entry) is not an escape. */
{
const double c[2] = {1.0, -2.0}; /* 1 - 2 theta */
CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 0,
"entry is not an escape");
}
}
/* Flat worldtube fixture with constant-velocity ends, optional motion
* segments (piecewise-constant velocity/radius_rate) and several ends. */
#define EVENT_ENDS 2
typedef struct {
size_t end_count;
SpacetimeEndId id[EVENT_ENDS];
double center[EVENT_ENDS][3];
double velocity[EVENT_ENDS][3];
double radius_a[EVENT_ENDS];
double radius_rate_a[EVENT_ENDS];
double radius_b[EVENT_ENDS];
double radius_rate_b[EVENT_ENDS];
double segment_t[EVENT_ENDS];
double t_ref[EVENT_ENDS]; /* center reference time for the moving end */
int has_segment[EVENT_ENDS];
int velocity_constant[EVENT_ENDS];
} EventContext;
static SpacetimePointStatus event_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
(void)source;
(void)t;
(void)x;
*metric = flat_metric();
return SPACETIME_POINT_OK;
}
static SpacetimeRayStatus event_classify(const SpacetimeSource *source,
double t, const double x[3]) {
(void)source;
(void)t;
(void)x;
return SPACETIME_RAY_ACTIVE;
}
static size_t event_end_count(const SpacetimeSource *source) {
return ((const EventContext *)source->context)->end_count;
}
static int event_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
const EventContext *c = source->context;
if (index >= c->end_count)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = c->id[index],
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int event_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const EventContext *c = source->context;
for (size_t i = 0; i < c->end_count; ++i) {
if (c->id[i] != end_id)
continue;
double radius = c->radius_a[i];
double radius_rate = c->radius_rate_a[i];
if (c->has_segment[i] && t < c->segment_t[i]) {
radius = c->radius_b[i] + c->radius_rate_b[i] * (t - c->segment_t[i]);
radius_rate = c->radius_rate_b[i];
}
const double dt = t - c->t_ref[i];
*out = (SpacetimeEscapeWorldtubeSample){
.center = {c->center[i][0] + c->velocity[i][0] * dt,
c->center[i][1] + c->velocity[i][1] * dt,
c->center[i][2] + c->velocity[i][2] * dt},
.velocity = {c->velocity[i][0], c->velocity[i][1], c->velocity[i][2]},
.radius = radius,
.radius_rate = radius_rate,
.velocity_constant = c->velocity_constant[i],
.valid = 1};
return 0;
}
return -1;
}
static double event_next_segment(const SpacetimeSource *source,
SpacetimeEndId end_id, double t) {
const EventContext *c = source->context;
for (size_t i = 0; i < c->end_count; ++i)
if (c->id[i] == end_id && c->has_segment[i] && t > c->segment_t[i])
return c->segment_t[i];
return NAN;
}
static void event_destroy(SpacetimeSource *source) {
source->context = NULL;
source->ops = NULL;
}
static const SpacetimeOps event_ops = {
.eval = event_eval,
.classify = event_classify,
.asymptotic_end_count = event_end_count,
.asymptotic_end = event_end,
.escape_worldtube_sample = event_worldtube,
.escape_worldtube_next_segment = event_next_segment,
.destroy = event_destroy,
};
/* Two ends at the same center: end id 0 is the smaller (nearer) sphere, so it
* is exited first no matter which descriptor position it occupies. */
static void test_event_two_ends_order(void) {
for (int swap = 0; swap < 2; ++swap) {
EventContext c;
memset(&c, 0, sizeof c);
c.end_count = 2;
c.velocity_constant[0] = c.velocity_constant[1] = 1;
const SpacetimeEndId near_id = 3, far_id = 7;
const double near_radius = 3.0, far_radius = 5.0;
if (!swap) {
c.id[0] = near_id;
c.radius_a[0] = near_radius;
c.id[1] = far_id;
c.radius_a[1] = far_radius;
} else {
c.id[0] = far_id;
c.radius_a[0] = far_radius;
c.id[1] = near_id;
c.radius_a[1] = near_radius;
}
SpacetimeSource source = {.ops = &event_ops, .context = &c};
GeodesicRayState state;
fixture_state(2.0, &state);
GeodesicTraceConfig config = dp_config(1e-10, 10.0, 1.0e6);
config.max_step = 10.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, "two-end slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -20.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"two-end escape terminates");
CHECK(out.end_id == near_id,
"earlier end wins regardless of descriptor order");
CHECK(fabs(out.stop_coordinate_time + 1.0) < 1e-6,
"two-end crossing is the nearest surface");
}
}
/* Piecewise-constant worldtube: the step must be clamped to the segment
* boundary or the constant-velocity polynomial extrapolates the wrong radius
* and predicts the wrong crossing. */
static void test_event_segmented_worldtube(void) {
EventContext c;
memset(&c, 0, sizeof c);
c.end_count = 1;
c.id[0] = 0;
c.velocity_constant[0] = 1;
c.radius_a[0] = 5.0;
c.radius_rate_a[0] = 0.0;
c.radius_b[0] = 5.0;
c.radius_rate_b[0] = 1.0;
c.segment_t[0] = -2.0;
c.has_segment[0] = 1;
SpacetimeSource source = {.ops = &event_ops, .context = &c};
GeodesicRayState state;
fixture_state(2.0, &state);
GeodesicTraceConfig config = dp_config(1e-10, 4.0, 1.0e6);
config.max_step = 4.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0,
"segment slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -20.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"segmented worldtube escape");
CHECK(fabs(out.stop_coordinate_time + 2.5) < 1e-6,
"segmented crossing uses the second segment");
}
/* Arbitrary accelerated worldtubes are explicitly unsupported rather than
* routed with a coarse sign search that can miss a narrow first entry. */
static void test_event_nonconstant_unsupported(void) {
EventContext c;
memset(&c, 0, sizeof c);
c.end_count = 1;
c.id[0] = 0;
c.radius_a[0] = 5.0;
c.velocity_constant[0] = 0; /* arbitrary acceleration */
SpacetimeSource source = {.ops = &event_ops, .context = &c};
/* Pre-route from outside must report UNSUPPORTED, not a fabricated miss. */
const ObserverState outside = flat_observer_at((double[]){10.0, 0.0, 0.0});
AsymptoticRoute route;
CHECK(asymptotic_route_camera(&source, &outside, (double[]){-1.0, 0.0, 0.0},
&route) == ASYMPTOTIC_UNSUPPORTED,
"nonconstant preroute is unsupported");
/* An inside state that reaches the advance loop is rejected there too. */
GeodesicRayState state;
fixture_state(1.0, &state);
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6);
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0,
"nonconstant slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -5.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_UNSUPPORTED,
"nonconstant advance is an explicit unsupported failure");
}
/* Synthetic curved metric with alpha = 1 - k x. The Eulerian energy
* L = ln(alpha p^0) grows along the past-directed ray, so a threshold crossing
* and a worldtube escape can be placed in the same accepted step. */
typedef struct {
double k;
double radius;
int has_bad;
double bad_lo, bad_hi;
} AlphaContext;
static SpacetimePointStatus alpha_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
const AlphaContext *c = source->context;
if (c->has_bad && t >= c->bad_lo && t <= c->bad_hi)
return SPACETIME_POINT_OUT_OF_DOMAIN;
const double alpha = 1.0 - c->k * x[0];
if (!(alpha > 0.0))
return SPACETIME_POINT_INVALID_METRIC;
*metric = flat_metric();
metric->alpha = alpha;
metric->d_alpha[0] = -c->k;
return SPACETIME_POINT_OK;
}
static size_t alpha_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int alpha_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
(void)source;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int alpha_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const AlphaContext *c = source->context;
(void)t;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = c->radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static const SpacetimeOps alpha_ops = {
.eval = alpha_eval,
.classify = event_classify,
.asymptotic_end_count = alpha_end_count,
.asymptotic_end = alpha_end,
.escape_worldtube_sample = alpha_worldtube,
.destroy = event_destroy,
};
static void alpha_state(double x0, double k, GeodesicRayState *state) {
memset(state, 0, sizeof *state);
state->x[0] = x0;
state->Pi[0] = -1.0;
state->log_alpha_p0 = log(1.0 - k * x0);
state->log_alpha_p0_0 = state->log_alpha_p0;
}
/* Threshold and escape in the same accepted step: the earlier event wins. */
static void test_event_threshold_order(void) {
struct {
double radius;
RayOutcome outcome;
const char *label;
} cases[2] = {{0.5, RAY_OUTCOME_DARK, "threshold first"},
{0.3, RAY_OUTCOME_ESCAPED, "escape first"}};
for (int i = 0; i < 2; ++i) {
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = cases[i].radius;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(1e-4, 1.0, 1.0e6);
config.max_step = 1.0;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 0.35,
.policy_version = 1};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0,
"threshold slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -3.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == cases[i].outcome,
cases[i].label);
if (out.outcome == RAY_OUTCOME_DARK) {
CHECK(out.accepted_steps == 1u,
"threshold and escape were compared within one accepted step");
CHECK(out.reason == RAY_REASON_REDSHIFT_LIMIT, "dark reason");
CHECK(out.threshold_value >= 0.35 &&
out.threshold_value < 0.35 + 0.05,
"dark records the actual threshold value");
}
}
}
/* A trial whose stages cross the threshold but is rejected must not produce a
* dark terminal. Here the long first trial hits a forbidden window, is
* rejected and shrunk to a step that stays below the threshold; with a
* one-step budget the ray ends UNRESOLVED, not DARK. */
static void test_event_rejected_stage_threshold(void) {
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = 10.0;
c.has_bad = 1;
c.bad_lo = -0.81;
c.bad_hi = -0.79;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(1e-9, 1.0, 1.0e6);
config.max_step = 1.0;
config.max_steps = 1;
/* Threshold crossing near s = 0.8, well beyond the shrunk accepted step. */
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 0.8,
.policy_version = 1};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -2.0, &slab) == 0,
"reject threshold slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -2.0, &config, &out);
spacetime_free_slab(slab);
CHECK(state.rejected_steps > 0u, "long trial was rejected");
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_UNRESOLVED,
"rejected trial does not create DARK");
CHECK(out.reason == RAY_REASON_BUDGET_EXHAUSTED,
"unresolved reason is budget exhaustion");
}
/* ------------------------------------------------------------------ */
/* 10. RK4 real RHS accounting and explicit unknown-stepper rejection */
/* ------------------------------------------------------------------ */
static void test_rk4_rhs_cost(void) {
/* Four fixed RK4 steps over a finite interval cost exactly four RHS each. */
{
SpacetimeSource source = {0};
CHECK(spacetime_create_minkowski(&source, 1.0e9) == 0, "create minkowski");
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config;
memset(&config, 0, sizeof config);
config.stepper = GEODESIC_STEPPER_RK4;
config.coordinate_time_step = 0.25;
config.max_steps = 100;
config.threshold.kind = THRESHOLD_DISABLED;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "rk4 slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_ACTIVE, "rk4 finite interval is active");
CHECK(state.steps == 4u, "rk4 took four accepted steps");
CHECK(state.rhs_evaluations == 16u, "rk4 four steps cost sixteen RHS");
CHECK(state.rejected_steps == 0u, "rk4 never rejects");
spacetime_destroy(&source);
}
/* A stage failure costs the real number of evaluations performed. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN;
context.bad_lo = -1.0e30;
context.bad_hi = -0.01; /* stage 1 at t = -0.05 fails, the initial is OK */
context.radius = 1.0e9;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(0.0, &state);
GeodesicTraceConfig config;
memset(&config, 0, sizeof config);
config.stepper = GEODESIC_STEPPER_RK4;
config.coordinate_time_step = 0.1;
config.max_steps = 10;
config.threshold.kind = THRESHOLD_DISABLED;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"rk4 failure slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_OUT_OF_DOMAIN,
"rk4 failed stage reports the point status");
CHECK(state.rhs_evaluations == 2u,
"rk4 failed stage-1 cost is the real two RHS");
CHECK(state.rejected_steps == 0u, "rk4 failure is not a rejection");
}
/* Escape localization adds its own real RHS cost on top of the steps. */
{
FixtureContext context;
memset(&context, 0, sizeof context);
context.radius = 5.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &context};
GeodesicRayState state;
fixture_state(2.0, &state);
GeodesicTraceConfig config;
memset(&config, 0, sizeof config);
config.stepper = GEODESIC_STEPPER_RK4;
config.coordinate_time_step = 0.5;
config.max_steps = 100;
config.threshold.kind = THRESHOLD_DISABLED;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0,
"rk4 escape slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -20.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"rk4 escape still terminates");
CHECK(out.accepted_steps > 0u &&
out.rhs_evaluations > 4ul * out.accepted_steps,
"rk4 escape localization adds real RHS cost");
}
}
static void test_unknown_stepper_rejected(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski");
const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0});
GeodesicTraceConfig config;
memset(&config, 0, sizeof config);
config.stepper = (GeodesicStepper)99;
config.coordinate_time_step = 0.25;
config.max_steps = 10;
config.threshold.kind = THRESHOLD_DISABLED;
const RayEndpoint traced = geodesic_trace_past(
&source, &observer, (double[]){1.0, 0.0, 0.0}, &config);
CHECK(traced.outcome == RAY_OUTCOME_INCOMPLETE &&
traced.reason == RAY_REASON_PROTOCOL_ERROR,
"trace_past rejects an unknown stepper");
GeodesicRayState state;
fixture_state(0.0, &state);
const RayEndpoint resumed =
geodesic_trace_past_from_state(&source, &state, &config);
CHECK(resumed.outcome == RAY_OUTCOME_INCOMPLETE &&
resumed.reason == RAY_REASON_PROTOCOL_ERROR,
"trace_past_from_state rejects an unknown stepper");
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "unknown slab");
RayEndpoint advanced = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &advanced);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
advanced.outcome == RAY_OUTCOME_INCOMPLETE &&
advanced.reason == RAY_REASON_PROTOCOL_ERROR,
"advance rejects an unknown stepper");
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* 11. Production advance on a curved synthetic metric: real single- */
/* step inside->outside->inside, tangent, and endpoint roots. */
/* */
/* gamma = I, alpha = 1, K = 0, beta(t) = (3 - 8 (t0 - t), 0, 0) with */
/* no spatial beta derivative. This is a flat metric in accelerating */
/* translated coordinates: Pi and L are constant, the past spatial path */
/* is x(s) = 4 s (1 - s) for s = t0 - t in [0, 1]. It is a geometric */
/* numerical fixture for the production event layer, not a physical */
/* outer-region model; the terminal Minkowski conversion only reads the */
/* legal local metric at the crossing. */
/* ------------------------------------------------------------------ */
typedef struct {
double t0;
double radius;
} CurvedContext;
static SpacetimePointStatus curved_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
const CurvedContext *c = source->context;
(void)x;
*metric = flat_metric();
metric->beta[0] = 3.0 - 8.0 * (c->t0 - t);
return SPACETIME_POINT_OK;
}
static size_t curved_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int curved_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
(void)source;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int curved_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const CurvedContext *c = source->context;
(void)t;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = c->radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static const SpacetimeOps curved_ops = {
.eval = curved_eval,
.classify = event_classify,
.asymptotic_end_count = curved_end_count,
.asymptotic_end = curved_end,
.escape_worldtube_sample = curved_worldtube,
.destroy = event_destroy,
};
/* Null past state: Pi = (-1, 0, 0) has gamma^{ij} Pi_i Pi_j = 1. */
static void curved_state(GeodesicRayState *state) {
memset(state, 0, sizeof *state);
state->Pi[0] = -1.0;
}
/* The earliest exit of the inside->outside->inside path is s = 0.1464466. */
static double curved_first_exit_s(void) { return (1.0 - sqrt(0.5)) / 2.0; }
static void test_event_curved_in_out_in(void) {
CurvedContext c = {.t0 = 0.0, .radius = 0.5};
SpacetimeSource source = {.ops = &curved_ops, .context = &c};
GeodesicRayState state;
curved_state(&state);
const double expected_s = curved_first_exit_s();
GeodesicTraceConfig config = dp_config(1e-10, 1.0, 1.0e6);
config.max_step = 1.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "curved slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"curved in-out-in escapes at the first exit");
CHECK(out.end_id == 0, "curved escape end id");
CHECK(fabs(out.final_x[0] - 0.5) < 1e-6,
"curved earliest exit position");
CHECK(fabs(out.stop_coordinate_time + expected_s) < 1e-6,
"curved earliest exit time");
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, state.x, &metric) == SPACETIME_POINT_OK &&
fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12,
"curved fixture state is null");
}
static void test_event_curved_tangent(void) {
/* The path peaks at x = 1, exactly touching the R = 1 sphere at s = 0.5.
* The tangent touch must not terminate the ray. */
CurvedContext c = {.t0 = 0.0, .radius = 1.0};
SpacetimeSource source = {.ops = &curved_ops, .context = &c};
GeodesicRayState state;
curved_state(&state);
GeodesicTraceConfig config = dp_config(1e-10, 1.0, 1.0e6);
config.max_step = 1.0;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "tangent slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_ACTIVE,
"tangent touch does not escape");
CHECK(state.coordinate_time == -1.0,
"tangent trace reached the slab boundary");
}
static void test_event_curved_endpoint_root(void) {
/* Truncate the step exactly at the first exit so F == 0 lands on the step
* endpoint. The in-step enumeration must not accept it; the following step
* starts at the boundary root and escapes reliably. */
const double expected_s = curved_first_exit_s();
CurvedContext c = {.t0 = 0.0, .radius = 0.5};
SpacetimeSource source = {.ops = &curved_ops, .context = &c};
GeodesicRayState state;
curved_state(&state);
GeodesicTraceConfig config = dp_config(1e-12, expected_s, 1.0e6);
config.max_step = expected_s;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"endpoint slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"endpoint root escapes on a later step");
CHECK(out.accepted_steps == 2u,
"endpoint root is detected on the following boundary step");
CHECK(fabs(out.stop_coordinate_time + expected_s) < 1e-6,
"endpoint root crossing time");
CHECK(fabs(out.final_x[0] - 0.5) < 1e-6, "endpoint root position");
}
/* LOG_P0 is not polynomial in the dense output, so under DP54 it is still only
* checked on trusted accepted states; it is not the CLI default and needs no
* dense candidate. */
static void test_event_log_p0_accepted_state(void) {
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = 10.0;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(1e-6, 0.25, 1.0e6);
config.max_step = 0.25;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_P0,
.value = 0.2,
.policy_version = 1};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "log_p0 slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_DARK,
"LOG_P0 truncates on an accepted state");
CHECK(out.reason == RAY_REASON_REDSHIFT_LIMIT, "LOG_P0 dark reason");
CHECK(out.threshold_value >= 0.2,
"LOG_P0 records the reached monitored value");
}
/* ------------------------------------------------------------------ */
/* 12. Review-fix regressions: representable step times, absorbed */
/* endpoint roots, and actual-time escape/threshold ordering. */
/* ------------------------------------------------------------------ */
/* A nominal step must commit the representable interval it actually
* integrated: x must equal the elapsed coordinate time to local ULP even when
* t + h rounds badly. */
static void test_event_step_time_precision(void) {
const double origins[3] = {0.0, 1.0e12, 1.0e14};
for (int i = 0; i < 3; ++i) {
FixtureContext c;
memset(&c, 0, sizeof c);
c.radius = 1.0e6;
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.coordinate_time = origins[i];
state.integration_start_time = origins[i];
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-9, 0.1, 10.0);
config.max_step = 0.1;
config.max_steps = 10;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, origins[i], origins[i] - 2.0, &slab) == 0,
"time-precision slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r = geodesic_advance_past_ray(
slab, &state, origins[i] - 2.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_UNRESOLVED,
"ten-step budget is unresolved");
CHECK(state.steps == 10u, "ten accepted steps");
const double elapsed = origins[i] - state.coordinate_time;
CHECK(fabs(state.x[0] - elapsed) < 1e-10,
"committed position matches elapsed coordinate time");
MetricData metric;
CHECK(spacetime_eval(&source, state.coordinate_time, state.x, &metric) ==
SPACETIME_POINT_OK &&
fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12,
"time-precision fixture stays null");
}
}
/* A crossing whose dense root is absorbed at theta == 1 (F_after strictly
* positive) must still escape via the actual-trajectory localizer. */
static void test_event_absorbed_endpoint_exit(void) {
double radius = 1.0;
for (int j = 0; j < 3; ++j)
radius = nextafter(radius, 0.0);
FixtureContext c;
memset(&c, 0, sizeof c);
c.radius = radius;
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-9, 1.0, 10.0);
config.max_step = 1.0;
config.max_steps = 4;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0,
"absorbed-root slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -5.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"absorbed endpoint root still escapes");
CHECK(fabs(out.final_x[0] - radius) <= 4.0 * DBL_EPSILON,
"absorbed-root crossing stays on the surface");
}
/* A start within the local geometric ULP outside the boundary is a boundary
* state and must escape when it moves outside. */
static void test_event_boundary_roundoff_exit(void) {
FixtureContext c;
memset(&c, 0, sizeof c);
c.radius = 1.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.x[0] = nextafter(1.0, 2.0);
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 10.0);
config.max_step = 0.5;
config.max_steps = 4;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0,
"roundoff slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -5.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"boundary roundoff start escapes outward");
}
/* A single-end state clearly outside its worldtube is a protocol error, not an
* unbounded search that can only end as budget exhaustion. */
static void test_event_clearly_outside_protocol(void) {
FixtureContext c;
memset(&c, 0, sizeof c);
c.radius = 1.0;
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.x[0] = 2.0;
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-9, 0.5, 10.0);
config.max_step = 0.5;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0,
"outside slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -5.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_PROTOCOL_ERROR,
"clearly-outside single end is a protocol error");
}
/* Escape and threshold are both localized on the actual trajectory and ordered
* by real coordinate time; the ODE tolerance is not a time tie. */
static void test_event_threshold_actual_order(void) {
const double exact = log(0.8 / 0.5);
const double tols[2] = {1e-4, 1e-9};
for (int t = 0; t < 2; ++t) {
/* Below the escape energy -> DARK first, even at loose tolerance. */
{
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = 0.5;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(tols[t], 1.0, 10.0);
config.max_step = 1.0;
config.threshold =
(ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = exact - 2.0e-5,
.policy_version = 3};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0,
"order dark slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -3.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_DARK &&
out.reason == RAY_REASON_REDSHIFT_LIMIT,
"sub-escape threshold is DARK");
CHECK(out.threshold_value >= exact - 2.0e-5,
"DARK records a reached threshold");
CHECK(out.stop_coordinate_time > -exact,
"DARK stop precedes the escape crossing");
}
/* Above the escape energy -> ESCAPED. */
{
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = 0.5;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(tols[t], 1.0, 10.0);
config.max_step = 1.0;
config.threshold =
(ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = exact + 1.0e-5,
.policy_version = 3};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0,
"order escape slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -3.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"super-escape threshold escapes");
}
}
}
/* ------------------------------------------------------------------ */
/* 13. Non-monotonic L pulse: a mid-trace dark truncation must fire */
/* even when the accepted endpoint returns below the threshold. */
/* */
/* Time-dependent gamma metric: alpha = 1, beta = 0, gamma_ij = */
/* a(t)^2 delta_ij, K_ij = -a(t)^2 H(t) delta_ij with a = exp(-F) and */
/* H = d ln a/dt = F'(s), s = t0 - t. Spatial derivatives are zero. */
/* With Pi_i = a n_i the past path has dL/ds = F'(s), so L(s) = F(s): */
/* the metric's gamma time dependence and K are consistent (a partial */
/* alpha(t) with K = 0 would not produce this evolution). */
/* ------------------------------------------------------------------ */
typedef struct {
double A;
double t0;
double radius;
int kind; /* 0 = two-pulse 16 A s(1-s)(s-.5)^2, 1 = single 4 A s(1-s) */
size_t end_count;
} CosmoContext;
static double cosmo_amp(double s, double A, int kind) {
if (kind == 0)
return 16.0 * A * s * (1.0 - s) * (s - 0.5) * (s - 0.5);
if (kind == 2)
return A * sin(6.283185307179586476925286766559 * s);
return 4.0 * A * s * (1.0 - s);
}
static double cosmo_amp_prime(double s, double A, int kind) {
if (kind == 0)
return 16.0 * A * (-4.0 * s * s * s + 6.0 * s * s - 2.5 * s + 0.25);
if (kind == 2)
return A * 6.283185307179586476925286766559 *
cos(6.283185307179586476925286766559 * s);
return 4.0 * A * (1.0 - 2.0 * s);
}
static SpacetimePointStatus cosmo_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
const CosmoContext *c = source->context;
(void)x;
const double s = c->t0 - t;
const double F = cosmo_amp(s, c->A, c->kind);
const double H = cosmo_amp_prime(s, c->A, c->kind);
const double a = exp(-F);
const double g = a * a;
*metric = flat_metric();
metric->alpha = 1.0;
metric->gamma[0][0] = metric->gamma[1][1] = metric->gamma[2][2] = g;
metric->K[0][0] = metric->K[1][1] = metric->K[2][2] = -g * H;
return SPACETIME_POINT_OK;
}
static size_t cosmo_end_count(const SpacetimeSource *source) {
return ((const CosmoContext *)source->context)->end_count;
}
static int cosmo_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
const CosmoContext *c = source->context;
if (index >= c->end_count)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int cosmo_worldtube(const SpacetimeSource *source, SpacetimeEndId end_id,
double t, SpacetimeEscapeWorldtubeSample *out) {
const CosmoContext *c = source->context;
(void)t;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = c->radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static const SpacetimeOps cosmo_ops = {
.eval = cosmo_eval,
.classify = event_classify,
.asymptotic_end_count = cosmo_end_count,
.asymptotic_end = cosmo_end,
.escape_worldtube_sample = cosmo_worldtube,
.destroy = event_destroy,
};
/* Analytic first upcrossing of F = threshold for each pulse shape. */
static double cosmo_first_crossing(double A, double threshold, int kind) {
if (kind == 0) {
const double disc = 0.0625 - threshold / (4.0 * A);
return 0.5 - sqrt((0.25 + sqrt(disc)) / 2.0);
}
return (1.0 - sqrt(1.0 - threshold / A)) / 2.0;
}
static void cosmo_run(double A, double threshold, int kind, size_t end_count,
double tol, int expect_dark) {
CosmoContext c;
memset(&c, 0, sizeof c);
c.A = A;
c.t0 = 0.0;
c.radius = 100.0;
c.kind = kind;
c.end_count = end_count;
SpacetimeSource source = {.ops = &cosmo_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.Pi[0] = -1.0; /* null: gamma^{ij} Pi_i Pi_j = |Pi|^2/a^2 = 1 */
state.log_alpha_p0 = 0.0;
state.log_alpha_p0_0 = 0.0;
GeodesicTraceConfig config = dp_config(tol, 1.0, 10.0);
config.max_step = 1.0;
config.max_steps = 100;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = threshold,
.policy_version = 1};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "cosmo slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED, "cosmo terminates");
if (!expect_dark) {
CHECK(out.outcome != RAY_OUTCOME_DARK, "cosmo expected non-dark");
return;
}
CHECK(out.outcome == RAY_OUTCOME_DARK &&
out.reason == RAY_REASON_REDSHIFT_LIMIT,
"mid-pulse dark truncation fires");
CHECK(out.threshold_value >= threshold,
"dark records a reached threshold");
const double s_ref = cosmo_first_crossing(A, threshold, kind);
CHECK(fabs(-out.stop_coordinate_time - s_ref) < 1e-5,
"dark stop is the first upcrossing");
CHECK(-out.stop_coordinate_time < 0.1464466,
"dark stop is before the first peak");
}
static void test_event_nonmonotonic_threshold(void) {
const double A = 0.08;
const double T = 0.01;
/* Both accepted-step tolerances must find the same first crossing even
* though the accepted endpoint returns to L = 0 < T. */
cosmo_run(A, T, 0, 1, 1e-3, 1);
cosmo_run(A, T, 0, 1, 1e-9, 1);
/* The same energy policy applies to a backend that declares no ends. */
cosmo_run(A, T, 0, 0, 1e-3, 1);
/* A single non-monotonic pulse (one upcrossing) behaves the same way. */
cosmo_run(A, T, 1, 1, 1e-3, 1);
cosmo_run(A, T, 1, 1, 1e-9, 1);
}
/* Large-origin moving worldtube: the production dense polynomial must match
* the actual F at the same theta, so the event timing cannot drift with the
* rounding of the sampled half-step. */
static void test_event_worldtube_polynomial_time_origin(void) {
const double t0 = 1.0e14;
EventContext c;
memset(&c, 0, sizeof c);
c.end_count = 1;
c.id[0] = 0;
c.center[0][0] = 0.0;
c.velocity[0][0] = -0.99;
c.t_ref[0] = t0;
c.radius_a[0] = 1.0;
c.velocity_constant[0] = 1;
SpacetimeSource source = {.ops = &event_ops, .context = &c};
GeodesicTraceConfig config = dp_config(1e-9, 0.05, 1.0e6);
config.max_step = 0.05;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, t0, t0 - 1.0, &slab) == 0, "probe slab");
/* Only theta values whose target t_before + h*theta is exactly representable
* compare cleanly; 1/3 lands one ULP inside the step for h = 3 ULP. */
const double thetas[3] = {0.0, 1.0 / 3.0, 1.0};
for (int k = 0; k < 3; ++k) {
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.coordinate_time = t0;
state.integration_start_time = t0;
state.x[0] = 0.5;
state.Pi[0] = -1.0;
double dense_value = NAN, actual_value = NAN;
CHECK(geodesic_event_worldtube_dense_probe(slab, &config, 0, &state,
t0 - 1.0, thetas[k],
&dense_value, &actual_value) == 1,
"worldtube dense probe runs");
const double scale = fmax(1.0, fabs(actual_value));
CHECK(fabs(dense_value - actual_value) < 1e-10 * scale,
"dense worldtube F matches actual F at a large time origin");
}
spacetime_free_slab(slab);
}
/* Explicitly prove the loose-tolerance pulse step is accepted as one step
* with both endpoints below the threshold (so the test covers the miss). */
static void test_event_nonmonotonic_accepts_one_step(void) {
const double A = 0.08;
const double T = 0.01;
CosmoContext c;
memset(&c, 0, sizeof c);
c.A = A;
c.radius = 100.0;
c.kind = 0;
c.end_count = 1;
SpacetimeSource source = {.ops = &cosmo_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-3, 1.0, 10.0);
config.max_step = 1.0;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = T,
.policy_version = 1};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"cosmo one-step slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_DARK,
"loose-tolerance pulse is dark");
CHECK(out.accepted_steps == 1u,
"loose tolerance accepts the whole pulse step in one step");
/* Both endpoints of the accepted step are genuinely below the threshold, so
* the dark stop can only come from the mid-step pulse. */
CHECK(cosmo_amp(0.0, A, 0) < T && cosmo_amp(1.0, A, 0) < T,
"pulse endpoints are below the threshold");
CHECK(out.threshold_value >= T, "pulse dark reached the threshold");
}
/* The threshold is a closed (>=) event: a crossing exactly at the step/slab
* endpoint must be accepted through the closed-end candidate, not sent into a
* permanent shrink. */
static void test_event_threshold_closed_endpoint(void) {
AlphaContext c;
memset(&c, 0, sizeof c);
c.k = 1.0;
c.radius = 10.0;
SpacetimeSource source = {.ops = &alpha_ops, .context = &c};
/* Phase 1: with the threshold disabled, find the exactly accepted endpoint
* monitored value at s = 1 (L - L0 = s along this ray). */
GeodesicRayState probe;
alpha_state(0.2, c.k, &probe);
GeodesicTraceConfig pconfig = dp_config(1e-4, 1.0, 10.0);
pconfig.max_step = 1.0;
pconfig.threshold.kind = THRESHOLD_DISABLED;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"closed endpoint slab");
RayEndpoint out = blank_endpoint();
(void)geodesic_advance_past_ray(slab, &probe, -1.0, &pconfig, &out);
const double endpoint_value = probe.log_alpha_p0 - probe.log_alpha_p0_0;
CHECK(fabs(probe.coordinate_time + 1.0) < 1e-9,
"endpoint probe reached the slab boundary");
CHECK(fabs(endpoint_value - 1.0) < 1e-9, "endpoint value is near one");
/* Phase 2: threshold set to that endpoint value: a closed (>=) event at the
* step/slab endpoint must be a DARK event, not a permanent shrink. */
GeodesicRayState state;
alpha_state(0.2, c.k, &state);
GeodesicTraceConfig config = dp_config(1e-4, 1.0, 10.0);
config.max_step = 1.0;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = endpoint_value,
.policy_version = 1};
(void)geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(out.outcome == RAY_OUTCOME_DARK &&
out.reason == RAY_REASON_REDSHIFT_LIMIT,
"endpoint threshold is a dark event");
CHECK(fabs(out.stop_coordinate_time + 1.0) < 1e-6,
"endpoint threshold is localized at the endpoint");
CHECK(out.threshold_value >= endpoint_value, "endpoint threshold reached");
}
/* When an event candidate cannot be confirmed on the actual trajectory and
* the bounded shrink is exhausted, this is an INCOMPLETE integration failure,
* never a rewritten DARK with a restored (steps = 0) state. */
static void test_event_threshold_confirmation_exhausted(void) {
CosmoContext c;
memset(&c, 0, sizeof c);
c.A = 0.1;
c.radius = 100.0;
c.kind = 2; /* non-polynomial L pulse; the coarse dense quartic cannot be
confirmed at this step/tolerance */
c.end_count = 1;
SpacetimeSource source = {.ops = &cosmo_ops, .context = &c};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"confirmation slab");
/* Phase 1: with the threshold disabled the same coarse step is accepted, so
* the phase-2 failure is an event-confirmation failure, not a step rejection. */
GeodesicRayState probe;
memset(&probe, 0, sizeof probe);
probe.Pi[0] = -1.0;
GeodesicTraceConfig pconfig = dp_config(1.0, 1.0, 10.0);
pconfig.max_step = 1.0;
pconfig.max_steps = 100;
pconfig.consecutive_rejection_limit = 1;
pconfig.atol_L = 1.0e6;
pconfig.rtol = 1.0e6;
pconfig.threshold.kind = THRESHOLD_DISABLED;
RayEndpoint probe_out = blank_endpoint();
(void)geodesic_advance_past_ray(slab, &probe, -1.0, &pconfig, &probe_out);
CHECK(probe.steps >= 1u, "the coarse step itself is accepted");
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1.0, 1.0, 10.0);
config.max_step = 1.0;
config.max_steps = 100;
config.consecutive_rejection_limit = 1;
config.atol_L = 1.0e6;
config.rtol = 1.0e6;
config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 0.13,
.policy_version = 1};
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -1.0, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_INTEGRATION_ERROR,
"unconfirmable threshold is an incomplete integration failure");
CHECK(out.outcome != RAY_OUTCOME_DARK,
"unconfirmable threshold is not rewritten into DARK");
CHECK(state.steps == 0u,
"last trusted state carries no fabricated accepted step");
CHECK(state.rejected_steps >= 1u, "rejection cost is real");
CHECK(out.rhs_evaluations > 0ul, "actual RHS cost is recorded");
}
/* Uniform unit scaling: with every length/time quantity scaled by 1e-9 the
* same flat escape must give event time/scale ~ 3 and x/scale ~ 5, i.e. no
* absolute coordinate-time floor breaks small units. */
static void test_event_unit_scaling(void) {
const double scale = 1.0e-9;
FixtureContext c;
memset(&c, 0, sizeof c);
c.radius = 5.0 * scale;
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
GeodesicRayState state;
memset(&state, 0, sizeof state);
state.x[0] = 2.0 * scale;
state.Pi[0] = -1.0;
GeodesicTraceConfig config = dp_config(1e-10, 0.5, 100.0);
config.coordinate_time_step *= scale;
config.min_step *= scale;
config.max_step *= scale;
config.atol_x *= scale;
config.max_lookback_time *= scale;
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -20.0 * scale, &slab) == 0,
"scaled slab");
RayEndpoint out = blank_endpoint();
const GeodesicAdvanceResult r =
geodesic_advance_past_ray(slab, &state, -20.0 * scale, &config, &out);
spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_TERMINATED &&
out.outcome == RAY_OUTCOME_ESCAPED,
"scaled flat escape");
CHECK(fabs((0.0 - out.stop_coordinate_time) / scale - 3.0) < 1e-6,
"scaled event time / scale is 3");
CHECK(fabs(out.final_x[0] / scale - 5.0) < 1e-6,
"scaled crossing position / scale is 5");
}
int main(void) {
test_minkowski_finite_interval();
test_schwarzschild_radial();
test_over_large_initial_step();
test_fixture_stage_retry();
test_fixture_fatal_and_bounds();
test_budget_lookback_and_resume();
test_flat_escape_localization();
test_event_time_translation();
test_event_subintegration_time_hole();
test_schwarzschild_dp_escape_matches_finish();
test_rk4_compatibility();
test_ray_pool_batch_matches_single();
test_ray_pool_continuation_state();
test_ray_pool_cross_slab_reject();
test_event_polynomial_roots();
test_event_two_ends_order();
test_event_segmented_worldtube();
test_event_nonconstant_unsupported();
test_event_threshold_order();
test_event_rejected_stage_threshold();
test_rk4_rhs_cost();
test_unknown_stepper_rejected();
test_event_curved_in_out_in();
test_event_curved_tangent();
test_event_curved_endpoint_root();
test_event_log_p0_accepted_state();
test_event_step_time_precision();
test_event_absorbed_endpoint_exit();
test_event_boundary_roundoff_exit();
test_event_clearly_outside_protocol();
test_event_threshold_actual_order();
test_event_nonmonotonic_threshold();
test_event_nonmonotonic_accepts_one_step();
test_event_worldtube_polynomial_time_origin();
test_event_threshold_closed_endpoint();
test_event_threshold_confirmation_exhausted();
test_event_unit_scaling();
if (failures != 0) {
fprintf(stderr, "geodesic adaptive regression: %d failure(s)\n", failures);
return 1;
}
puts("geodesic adaptive regression passed");
return 0;
}