#include "asymptotic.h" #include "observer.h" #include "ray.h" #include "spacetime.h" #include #include 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 ObserverState flat_observer(double x, double y, double z) { ObserverState o = {0}; o.coordinate_position[0] = x; o.coordinate_position[1] = y; o.coordinate_position[2] = z; 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; } /* Synthetic flat exterior with a Minkowski end whose worldtube center follows * x_c(t) = vx t + accel t^2 / 2. `constant` selects the closed quadratic path; * otherwise the generic bracketed driver runs. */ typedef struct { double vx; double accel; double radius; double radius_rate; double valid_t_min; int constant; double segment_t; /* Motion-segment boundary for the cross-segment test. */ int has_segment; int end_descriptor_fails; /* Protocol-error injection. */ int unsupported_kind; double invalid_center, invalid_halfwidth; /* Isolated invalid time window. */ int schwarzschild_kind; /* Declare a Schwarzschild monopole end. */ int sample_callback_fails; /* make escape_worldtube_sample return -1 */ int sample_invalid; /* valid = 0 */ int sample_nan_radius; int sample_nonpositive_radius; int fail_on_sample_call; /* 1-based callback invocation to fail. */ int sample_call_count; } SyntheticContext; static SpacetimePointStatus synthetic_eval(const SpacetimeSource *source, double t, const double x[3], MetricData *metric) { (void)source; (void)t; (void)x; *metric = (MetricData){.alpha = 1.0, .gamma = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; return SPACETIME_POINT_OK; } static SpacetimeRayStatus synthetic_classify(const SpacetimeSource *source, double t, const double x[3]) { (void)source; (void)t; (void)x; return SPACETIME_RAY_ACTIVE; } static size_t synthetic_end_count(const SpacetimeSource *source) { (void)source; return 1; } static int synthetic_end(const SpacetimeSource *source, size_t index, SpacetimeAsymptoticEnd *out) { const SyntheticContext *context = source->context; if (index != 0 || context->end_descriptor_fails) return -1; AsymptoticExteriorKind kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI; double mass = 0.0; if (context->unsupported_kind) { kind = (AsymptoticExteriorKind)999; } else if (context->schwarzschild_kind) { kind = ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE; mass = 1.0; } *out = (SpacetimeAsymptoticEnd){ .end_id = 0, .exterior_kind = kind, .mass = mass, .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 synthetic_worldtube(const SpacetimeSource *source, SpacetimeEndId end_id, double t, SpacetimeEscapeWorldtubeSample *out) { SyntheticContext *mutable_context = source->context; const SyntheticContext *context = mutable_context; if (end_id != 0) return -1; ++mutable_context->sample_call_count; if (context->fail_on_sample_call > 0 && mutable_context->sample_call_count == context->fail_on_sample_call) return -1; if (context->sample_callback_fails) return -1; if (context->sample_invalid) { *out = (SpacetimeEscapeWorldtubeSample){.valid = 0}; return 0; } if (context->sample_nan_radius) { *out = (SpacetimeEscapeWorldtubeSample){.radius = NAN, .valid = 1}; return 0; } if (context->sample_nonpositive_radius) { *out = (SpacetimeEscapeWorldtubeSample){.radius = 0.0, .valid = 1}; return 0; } if (!isfinite(t) || t < context->valid_t_min) { *out = (SpacetimeEscapeWorldtubeSample){.valid = 0}; return 0; } if (context->invalid_halfwidth > 0.0 && fabs(t - context->invalid_center) <= context->invalid_halfwidth) { *out = (SpacetimeEscapeWorldtubeSample){.valid = 0}; return 0; } if (context->has_segment && t < context->segment_t) { /* Second segment: center moves toward +x as t decreases. */ *out = (SpacetimeEscapeWorldtubeSample){ .center = {context->segment_t - t, 0.0, 0.0}, .velocity = {-1.0, 0.0, 0.0}, .radius = context->radius, .radius_rate = context->radius_rate, .velocity_constant = context->constant, .valid = 1}; return 0; } *out = (SpacetimeEscapeWorldtubeSample){ .center = {context->vx * t + 0.5 * context->accel * t * t, 0.0, 0.0}, .velocity = {context->vx + context->accel * t, 0.0, 0.0}, .radius = context->radius, .radius_rate = context->radius_rate, .velocity_constant = context->constant, .valid = 1}; return 0; } static double synthetic_next_segment(const SpacetimeSource *source, SpacetimeEndId end_id, double t) { const SyntheticContext *context = source->context; (void)end_id; if (context->has_segment && t > context->segment_t) return context->segment_t; return NAN; } static void synthetic_destroy(SpacetimeSource *source) { /* The test context lives on the stack, so it is not freed; but match the * real destroy postcondition. */ source->context = NULL; source->ops = NULL; } static const SpacetimeOps synthetic_ops = { .eval = synthetic_eval, .classify = synthetic_classify, .asymptotic_end_count = synthetic_end_count, .asymptotic_end = synthetic_end, .escape_worldtube_sample = synthetic_worldtube, .escape_worldtube_next_segment = synthetic_next_segment, .destroy = synthetic_destroy, }; static void test_fixed_sphere(void) { SpacetimeSource source = {0}; CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); AsymptoticRoute route; const ObserverState inside = flat_observer(0.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&source, &inside, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_INSIDE, "origin camera is inside"); const ObserverState outside = flat_observer(50.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&source, &outside, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "outside ray toward sphere enters"); CHECK(fabs(route.activate_t + 40.0) < 1e-9, "fixed-sphere entry time"); CHECK(fabs(route.x[0] - 10.0) < 1e-9 && fabs(route.x[1]) < 1e-9 && fabs(route.x[2]) < 1e-9, "fixed-sphere entry position"); CHECK(asymptotic_route_camera(&source, &outside, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "outside ray away misses"); CHECK(fabs(route.n_infinity[0] - 1.0) < 1e-12 && fabs(route.n_infinity[1]) < 1e-12, "miss direction"); CHECK(fabs(route.frequency_ratio - 1.0) < 1e-12, "flat frequency ratio"); const ObserverState tangent = flat_observer(50.0, 10.0, 0.0); CHECK(asymptotic_route_camera(&source, &tangent, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "tangent ray is not a crossing"); const ObserverState near_miss = flat_observer(50.0, 10.0 + 1e-6, 0.0); CHECK(asymptotic_route_camera(&source, &near_miss, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "near-tangent outside ray misses"); const ObserverState near_hit = flat_observer(50.0, 10.0 - 1e-6, 0.0); CHECK(asymptotic_route_camera(&source, &near_hit, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "near-tangent inside ray enters"); RayEndpoint endpoint; CHECK(asymptotic_finish_escape(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0}, (double[]){-1.0, 0.0, 0.0}, 0.0, &endpoint) == ASYMPTOTIC_OK && endpoint.outcome == RAY_OUTCOME_ESCAPED && endpoint.end_id == 0, "finish outward crossing"); CHECK(fabs(endpoint.n_infinity[0] - 1.0) < 1e-12 && fabs(endpoint.frequency_ratio - 1.0) < 1e-12, "finish direction and frequency"); spacetime_destroy(&source); } static void test_large_radius_quadratic(void) { SpacetimeSource source = {0}; CHECK(spacetime_create_minkowski(&source, 1.0e12) == 0, "create huge minkowski sphere"); AsymptoticRoute route; const ObserverState hit = flat_observer(2.0e12, 5.0e11, 0.0); CHECK(asymptotic_route_camera(&source, &hit, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "large-radius hit stays quadratic"); const ObserverState miss = flat_observer(2.0e12, 2.0e12, 0.0); CHECK(asymptotic_route_camera(&source, &miss, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "large-radius miss stays quadratic"); spacetime_destroy(&source); /* Small entry root a hair outside a large sphere: the cancellation-prone * case for the naive formula. */ SpacetimeSource big = {0}; CHECK(spacetime_create_minkowski(&big, 1.0e9) == 0, "create 1e9 sphere"); const ObserverState just_outside = flat_observer(1.0e9 + 1e-3, 0.0, 0.0); CHECK(asymptotic_route_camera(&big, &just_outside, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "just-outside hit"); const double expected_delta = just_outside.coordinate_position[0] - 1.0e9; CHECK(fabs(-route.activate_t - expected_delta) < 1e-7 + 1e-11 * fabs(expected_delta), "just-outside entry time within budget"); double residual; CHECK(asymptotic_worldtube_value(&big, route.end_id, route.activate_t, route.x, &residual) == 0 && fabs(residual) <= 1e-12 * 1.0e9 * 1.0e9, "just-outside entry on worldtube"); spacetime_destroy(&big); } static void test_boundary_semantics_minkowski(void) { SpacetimeSource source = {0}; CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); AsymptoticRoute route; const ObserverState on_boundary = flat_observer(10.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&source, &on_boundary, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_INSIDE, "on-boundary past-inward is inside"); CHECK(asymptotic_route_camera(&source, &on_boundary, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "on-boundary past-outward escapes"); CHECK(asymptotic_route_camera(&source, &on_boundary, (double[]){0.0, 1.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "on-boundary tangent escapes"); spacetime_destroy(&source); } static void test_boundary_semantics_generic(void) { /* velocity_constant == 0 forces the generic bracketed driver. A finite * history bounds the outward/tangent searches, which must not be reported * as entries (they end as TIME_RANGE_EXHAUSTED instead). */ SyntheticContext context = {.radius = 10.0, .valid_t_min = -100.0, .constant = 0}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState on_boundary = flat_observer(10.0, 0.0, 0.0); AsymptoticRoute route; const AsymptoticStatus inward = asymptotic_route_camera( &source, &on_boundary, (double[]){-1.0, 0.0, 0.0}, &route); CHECK(inward == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_INSIDE, "generic on-boundary inward is inside"); const AsymptoticStatus outward = asymptotic_route_camera( &source, &on_boundary, (double[]){1.0, 0.0, 0.0}, &route); CHECK(outward == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, "generic on-boundary outward is not an entry"); const AsymptoticStatus tangent = asymptotic_route_camera( &source, &on_boundary, (double[]){0.0, 1.0, 0.0}, &route); CHECK(tangent == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, "generic on-boundary tangent is not an entry"); } static void test_negative_radius_root_guard(void) { /* Deliberately bypasses a constructor: every sampled radius is finite and * positive, but the algebraic root sits where R < 0. The cheap root-level * guard must reject it instead of fabricating a negative-radius entry. */ SyntheticContext context = {.radius = 10.0, .radius_rate = 2.0, .valid_t_min = -1.0e30, .constant = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState on_boundary = flat_observer(10.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &on_boundary, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "negative-radius algebraic root is rejected"); } static void test_source_finalize(void) { /* A real constructor already finalizes: finalize is idempotent. */ SpacetimeSource good = {0}; CHECK(spacetime_create_minkowski(&good, 10.0) == 0, "create minkowski"); CHECK(spacetime_source_finalize(&good) == 0, "valid source finalizes"); spacetime_destroy(&good); /* A structurally valid synthetic source must pass, so the failure cases * below are attributable to their specific defect rather than to the test * ops themselves. */ SyntheticContext well_formed = {.radius = 10.0, .valid_t_min = -1.0e30, .constant = 1}; SpacetimeSource valid_source = {.ops = &synthetic_ops, .context = &well_formed}; CHECK(spacetime_source_finalize(&valid_source) == 0, "well-formed synthetic source finalizes"); /* Structural protocol errors must be rejected before any ray trace. */ SyntheticContext bad_kind = {.radius = 10.0, .valid_t_min = -1.0e30, .constant = 1, .unsupported_kind = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &bad_kind}; CHECK(spacetime_source_finalize(&source) != 0, "unsupported exterior kind fails finalize"); SyntheticContext bad_desc = {.radius = 10.0, .valid_t_min = -1.0e30, .constant = 1, .end_descriptor_fails = 1}; source = (SpacetimeSource){.ops = &synthetic_ops, .context = &bad_desc}; CHECK(spacetime_source_finalize(&source) != 0, "broken end descriptor fails finalize"); } static void test_motion_segment_domain(void) { /* Segment 1 (t >= -50) is a static R=10 sphere; its quadratic root lies at * s = 90, past the segment boundary. Segment 2 (t < -50) moves the center * with velocity -1, so the true entry is at s = 70. The result must come * from segment 2. */ SyntheticContext context = {.radius = 10.0, .valid_t_min = -1.0e30, .constant = 1, .segment_t = -50.0, .has_segment = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "entry found in the second motion segment"); CHECK(fabs(route.activate_t + 70.0) < 1e-6, "second-segment entry, not the stale first-segment root"); } static void test_schwarzschild_sample_failures(void) { const ObserverState camera = flat_observer(0.0, 0.0, 0.0); AsymptoticRoute route; SyntheticContext base = {.radius = 256.0, .valid_t_min = -1.0e30, .constant = 1, .schwarzschild_kind = 1}; SyntheticContext callback = base; callback.sample_callback_fails = 1; SpacetimeSource s1 = {.ops = &synthetic_ops, .context = &callback}; CHECK(asymptotic_route_camera(&s1, &camera, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "schwarzschild callback failure is invalid"); SyntheticContext invalid = base; invalid.sample_invalid = 1; SpacetimeSource s2 = {.ops = &synthetic_ops, .context = &invalid}; CHECK(asymptotic_route_camera(&s2, &camera, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, "schwarzschild valid=0 is exhausted"); SyntheticContext nan = base; nan.sample_nan_radius = 1; SpacetimeSource s3 = {.ops = &synthetic_ops, .context = &nan}; CHECK(asymptotic_route_camera(&s3, &camera, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "schwarzschild NaN radius is invalid"); SyntheticContext zero = base; zero.sample_nonpositive_radius = 1; SpacetimeSource s4 = {.ops = &synthetic_ops, .context = &zero}; CHECK(asymptotic_route_camera(&s4, &camera, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "schwarzschild non-positive radius is invalid"); /* Second call fails: the containment sample (#1) succeeds with the camera * outside, and schwarzschild_route's own sample (#2) is the one that fails. * This locks the dedicated Schwarzschild sample handling. */ SyntheticContext second = base; second.fail_on_sample_call = 2; SpacetimeSource s5 = {.ops = &synthetic_ops, .context = &second}; const ObserverState outside = flat_observer(500.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&s5, &outside, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "schwarzschild_route second-sample failure is invalid"); } static void test_end_protocol_error(void) { const ObserverState inside = flat_observer(0.0, 0.0, 0.0); AsymptoticRoute route; SyntheticContext bad = {.radius = 20.0, .valid_t_min = -1.0e30, .constant = 1, .end_descriptor_fails = 1}; SpacetimeSource bad_source = {.ops = &synthetic_ops, .context = &bad}; CHECK(asymptotic_route_camera(&bad_source, &inside, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_INVALID, "bad end descriptor is an explicit protocol error"); SyntheticContext unsupported = {.radius = 20.0, .valid_t_min = -1.0e30, .constant = 1, .unsupported_kind = 1}; SpacetimeSource unsupported_source = {.ops = &synthetic_ops, .context = &unsupported}; CHECK(asymptotic_route_camera(&unsupported_source, &inside, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_UNSUPPORTED, "unsupported exterior with camera inside is not silently accepted"); /* The lifecycle layer, not just the pre-route, must refuse legacy fallback * whenever ends are declared but broken. */ MetricSlab *slab = NULL; CHECK(spacetime_load_slab(&bad_source, 0.0, -10.0, &slab) == 0, "bad-descriptor slab"); GeodesicRayState state = {.coordinate_time = 0.0, .x = {1.0, 0.0, 0.0}, .Pi = {0.0, 0.0, 0.0}, .log_alpha_p0 = 0.0, .steps = 0}; const GeodesicTraceConfig config = {.coordinate_time_step = 1.0, .max_steps = 10}; RayEndpoint endpoint = {.frequency_ratio = 0.0, .magnification = 1.0, .end_id = SPACETIME_END_NONE, .outcome = RAY_OUTCOME_INCOMPLETE}; CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) == GEODESIC_ADVANCE_FAILED && endpoint.outcome == RAY_OUTCOME_INCOMPLETE && endpoint.reason == RAY_REASON_PROTOCOL_ERROR, "advance rejects a declared-but-broken end without legacy"); spacetime_free_slab(slab); } static void test_interior_crossing_bisection_failure(void) { /* radius 20.3 makes the exit land strictly between steps: the accepted * step goes from F < 0 (t = -119.7) to F > 0 (t = -120.7). The invalid * window sits on the first bisection midpoint (t = -120.2), while both * accepted-step endpoints stay valid. */ SyntheticContext context = {.radius = 20.3, .valid_t_min = -1.0e30, .constant = 1, .invalid_center = -120.2, .invalid_halfwidth = 0.05}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState observer = flat_observer(100.0, 0.0, 0.0); const GeodesicTraceConfig config = {.coordinate_time_step = 1.0, .max_steps = 2048}; const RayEndpoint endpoint = geodesic_trace_past( &source, &observer, (double[]){-1.0, 0.0, 0.0}, &config); CHECK(endpoint.outcome == RAY_OUTCOME_INCOMPLETE && endpoint.reason == RAY_REASON_TIME_RANGE_EXHAUSTED && endpoint.end_id == 0, "interior crossing bisection propagates history exhaustion"); } static void test_generic_bisection_failure(void) { /* The isolated invalid window lands on a bisection midpoint while the * bracket endpoints stay valid, so only the bisection can see it. With the * strict F < 0 entry test, the bracket is s = 80 (F == 0) to s = 90 * (F < 0), so the first midpoint is t = -85. */ SyntheticContext context = {.radius = 20.0, .valid_t_min = -1.0e30, .constant = 0, .invalid_center = -85.0, .invalid_halfwidth = 1.0}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, "generic worldtube bisection propagates sample failure"); } static void test_interior_history_exhaustion(void) { SyntheticContext context = {.radius = 20.0, .valid_t_min = -100.0, .constant = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState observer = flat_observer(100.0, 0.0, 0.0); const GeodesicTraceConfig config = {.coordinate_time_step = 1.0, .max_steps = 2048}; const RayEndpoint endpoint = geodesic_trace_past( &source, &observer, (double[]){-1.0, 0.0, 0.0}, &config); CHECK(endpoint.outcome == RAY_OUTCOME_INCOMPLETE && endpoint.reason == RAY_REASON_TIME_RANGE_EXHAUSTED && endpoint.end_id == 0, "interior worldtube history exhaustion on a single trace"); RayPool pool; CHECK(ray_pool_init(&pool, 1) == 0, "pool init"); CHECK(ray_pool_append(&pool, &observer, (double[]){-1.0, 0.0, 0.0}, 0, 0) == 0, "append exhaustion ray"); ray_pool_preroute(&pool, &source); CHECK(pool.status[0] == RAY_POOL_PENDING, "exhaustion ray pends entry"); MetricSlab *slab = NULL; CHECK(spacetime_load_slab(&source, -80.0, -3000.0, &slab) == 0, "exhaustion slab"); ray_pool_activate_in_time_range(&pool, slab); CHECK(pool.status[0] == RAY_POOL_ACTIVE, "exhaustion ray activates"); ray_pool_advance_active(&pool, slab, &config); CHECK(pool.endpoint[0].outcome == RAY_OUTCOME_INCOMPLETE && pool.endpoint[0].reason == RAY_REASON_TIME_RANGE_EXHAUSTED && pool.endpoint[0].end_id == 0 && pool.status[0] == RAY_POOL_TERMINATED, "interior worldtube history exhaustion on a RayPool"); spacetime_free_slab(slab); ray_pool_destroy(&pool); } static void test_round_trip(void) { SpacetimeSource source = {0}; CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); MetricData metric = {.alpha = 1.0, .gamma = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; AsymptoticPhotonState canonical; CHECK(asymptotic_canonical_from_backend(&source, 0, &metric, 0.0, (double[]){3.0, 4.0, 0.0}, (double[]){-0.6, 0.8, 0.0}, 0.25, &canonical) == 0, "backend to canonical"); double x[3], Pi[3], log_alpha_p0; CHECK(asymptotic_backend_from_canonical(&source, &metric, &canonical, x, Pi, &log_alpha_p0) == 0, "canonical to backend"); CHECK(fabs(x[0] - 3.0) < 1e-14 && fabs(x[1] - 4.0) < 1e-14 && fabs(Pi[0] + 0.6) < 1e-14 && fabs(Pi[1] - 0.8) < 1e-14 && fabs(log_alpha_p0 - 0.25) < 1e-14, "round trip matches"); spacetime_destroy(&source); } static void test_moving_sphere(void) { SyntheticContext context = {.vx = 0.5, .accel = 0.0, .radius = 25.0, .valid_t_min = -1.0e30, .constant = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; AsymptoticRoute route; const ObserverState head_on = flat_observer(100.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&source, &head_on, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "head-on moving-sphere entry"); CHECK(fabs(route.activate_t + 150.0) < 1e-9, "head-on entry time"); CHECK(fabs(route.x[0] + 50.0) < 1e-9, "head-on entry position"); double value; CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t, route.x, &value) == 0 && fabs(value) <= 1e-13 * 25.0 * 25.0, "head-on entry lies on worldtube"); const ObserverState transverse = flat_observer(0.0, 40.0, 0.0); CHECK(asymptotic_route_camera(&source, &transverse, (double[]){0.0, -1.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "transverse moving-sphere entry"); CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t, route.x, &value) == 0 && fabs(value) <= 1e-13 * 25.0 * 25.0, "transverse entry lies on worldtube"); const ObserverState away = flat_observer(100.0, 0.0, 0.0); CHECK(asymptotic_route_camera(&source, &away, (double[]){1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ESCAPED, "co-moving ray misses"); } static void test_accelerated_worldtube(void) { SyntheticContext context = {.vx = 0.0, .accel = 0.02, .radius = 20.0, .valid_t_min = -1.0e30, .constant = 0}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "accelerated worldtube entry"); double value; CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t, route.x, &value) == 0 && fabs(value) <= 1e-13 * 20.0 * 20.0, "accelerated entry lies on worldtube"); CHECK(route.activate_t < -40.0 && route.activate_t > -60.0, "accelerated entry time in range"); context.valid_t_min = -30.0; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, "exhausted history is not a miss"); } static void test_accelerated_segment(void) { SyntheticContext context = {.vx = 0.0, .accel = 0.0, .radius = 20.0, .valid_t_min = -1.0e30, .constant = 0, .segment_t = -50.0, .has_segment = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, "cross-segment entry"); CHECK(route.activate_t < context.segment_t, "entry lies past the motion-segment boundary"); CHECK(fabs(route.activate_t + 65.0) < 1e-6, "cross-segment entry time"); double value; CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t, route.x, &value) == 0 && fabs(value) <= 1e-13 * 20.0 * 20.0, "cross-segment entry on worldtube"); } static void test_ray_pool_lifecycle(void) { SpacetimeSource source = {0}; CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); const ObserverState observer = flat_observer(50.0, 0.0, 0.0); RayPool pool; CHECK(ray_pool_init(&pool, 2) == 0, "pool init"); CHECK(ray_pool_append(&pool, &observer, (double[]){-1.0, 0.0, 0.0}, 0, 0) == 0, "append hit"); CHECK(ray_pool_append(&pool, &observer, (double[]){1.0, 0.0, 0.0}, 0, 1) == 0, "append miss"); ray_pool_preroute(&pool, &source); CHECK(pool.status[0] == RAY_POOL_PENDING && pool.activate_t[0] < observer.coordinate_time - 1.0, "entry ray stays pending until entry time"); CHECK(pool.status[1] == RAY_POOL_TERMINATED && pool.endpoint[1].outcome == RAY_OUTCOME_ESCAPED, "miss ray escapes during pre-route"); MetricSlab *early = NULL; CHECK(spacetime_load_slab(&source, -20.0, -30.0, &early) == 0, "early slab"); ray_pool_activate_in_time_range(&pool, early); CHECK(pool.status[0] == RAY_POOL_PENDING, "entry ray not active early"); spacetime_free_slab(early); MetricSlab *covering = NULL; CHECK(spacetime_load_slab(&source, 0.0, -100.0, &covering) == 0, "covering slab"); ray_pool_activate_in_time_range(&pool, covering); CHECK(pool.status[0] == RAY_POOL_ACTIVE && fabs(pool.t[0] - pool.activate_t[0]) < 1e-30, "entry ray activates at entry time"); spacetime_free_slab(covering); ray_pool_destroy(&pool); spacetime_destroy(&source); } int main(void) { test_fixed_sphere(); test_large_radius_quadratic(); test_round_trip(); test_moving_sphere(); test_accelerated_worldtube(); test_accelerated_segment(); test_boundary_semantics_minkowski(); test_boundary_semantics_generic(); test_negative_radius_root_guard(); test_source_finalize(); test_motion_segment_domain(); test_schwarzschild_sample_failures(); test_end_protocol_error(); test_generic_bisection_failure(); test_interior_history_exhaustion(); test_interior_crossing_bisection_failure(); test_ray_pool_lifecycle(); if (failures == 0) puts("asymptotic regression passed"); else fprintf(stderr, "%d asymptotic regression failures\n", failures); return failures == 0 ? 0 : 1; }