Replace radius-only escape termination with a common asymptotic exterior protocol: declared ends, moving escape worldtubes, directed inside->outside crossings, and a PENDING_ENTRY lifecycle shared by single-frame and movie tracing. Add an analytic Carlson-integral Schwarzschild monopole exterior (angle primitive, bracketed turning radius, ingoing Kerr-Schild coordinate-time transfer, conserved-energy frequency) so a camera outside the escape sphere is traced through an entry event. Make the lifecycle tri-state (no ends / ready / protocol error), carry end_id through the endpoint and lens mesh, validate sources in constructors via spacetime_source_finalize(), and refresh the Schwarzschild reference images for the corrected finish.
495 lines
22 KiB
C
495 lines
22 KiB
C
#include "asymptotic.h"
|
|
#include "asymptotic_schwarzschild.h"
|
|
#include "geodesic.h"
|
|
#include "observer.h"
|
|
#include "spacetime.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.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 angle_between(const double a[3], const double b[3]) {
|
|
const double dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
const double cx = a[1] * b[2] - a[2] * b[1];
|
|
const double cy = a[2] * b[0] - a[0] * b[2];
|
|
const double cz = a[0] * b[1] - a[1] * b[0];
|
|
return atan2(sqrt(cx * cx + cy * cy + cz * cz), dot);
|
|
}
|
|
|
|
static void test_round_trip(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end descriptor");
|
|
SchwarzschildCanonical in = {.end_id = 0,
|
|
.t = 0.0,
|
|
.rho = 256.0,
|
|
.rhat = {1.0, 0.0, 0.0},
|
|
.Lhat = {0.0, 1.0, 0.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, &in, x, Pi,
|
|
&log_alpha_p0) == 0,
|
|
"state from canonical");
|
|
MetricData metric;
|
|
CHECK(spacetime_eval(&source, in.t, x, &metric) == 0, "metric");
|
|
SchwarzschildCanonical out;
|
|
CHECK(asymptotic_schwarzschild_canonical_from_state(
|
|
&end, &metric, in.t, x, Pi, log_alpha_p0, &out) == 0,
|
|
"canonical from state");
|
|
CHECK(fabs(out.beta - in.beta) < 1e-13, "beta round trip");
|
|
CHECK(fabs(out.energy - in.energy) < 1e-13, "energy round trip");
|
|
CHECK(out.radial_sign == in.radial_sign, "radial sign round trip");
|
|
const double axis = angle_between(out.rhat, in.rhat);
|
|
CHECK(axis < 1e-13, "position direction round trip");
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
static void test_finish_matches_integration(void) {
|
|
SpacetimeSource near = {0}, far = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&near, 1.0, 256.0, 1.5) == 0,
|
|
"create near");
|
|
CHECK(spacetime_create_schwarzschild_ks(&far, 1.0, 1.0e5, 1.5) == 0,
|
|
"create far");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&near, 0, &end) == 0, "near end");
|
|
|
|
const double betas[] = {0.0, 0.5, 4.0, 10.0, 30.0, 100.0, 250.0};
|
|
const int beta_count = (int)(sizeof betas / sizeof betas[0]);
|
|
for (int k = 0; k < beta_count; ++k) {
|
|
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 = betas[k],
|
|
.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,
|
|
"finish state build");
|
|
|
|
double n_analytic[3], freq_analytic;
|
|
CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_analytic,
|
|
&freq_analytic) == 0,
|
|
"analytic finish");
|
|
|
|
GeodesicRayState state = {.coordinate_time = 0.0,
|
|
.x = {x[0], x[1], x[2]},
|
|
.Pi = {Pi[0], Pi[1], Pi[2]},
|
|
.log_alpha_p0 = log_alpha_p0,
|
|
.steps = 0};
|
|
const GeodesicTraceConfig config = {.coordinate_time_step = 5.0,
|
|
.max_steps = 100000};
|
|
MetricSlab *slab = NULL;
|
|
CHECK(spacetime_load_slab(&far, 0.0, -1.0e6, &slab) == 0, "far slab");
|
|
RayEndpoint endpoint = {.frequency_ratio = 0, .magnification = 1.0,
|
|
.end_id = SPACETIME_END_NONE,
|
|
.status = RAY_ENDPOINT_INVALID};
|
|
const GeodesicAdvanceResult result =
|
|
geodesic_advance_past_ray(slab, &state, -1.0e6, &config, &endpoint);
|
|
spacetime_free_slab(slab);
|
|
CHECK(result == GEODESIC_ADVANCE_TERMINATED &&
|
|
endpoint.status == RAY_ENDPOINT_ESCAPED,
|
|
"far integration escapes");
|
|
/* Pipeline check only: the far integration at step 5 and escape radius
|
|
* 1e5 has its own O(1e-5..1e-3) error. Quantitative accuracy is checked
|
|
* against the high-precision reference constants below. */
|
|
const double angle_error =
|
|
angle_between(n_analytic, endpoint.n_infinity);
|
|
CHECK(angle_error < 1e-2, "finish direction matches far integration");
|
|
CHECK(fabs(freq_analytic - endpoint.frequency_ratio) /
|
|
freq_analytic < 1e-2,
|
|
"finish frequency matches far integration");
|
|
(void)angle_error;
|
|
}
|
|
spacetime_destroy(&near);
|
|
spacetime_destroy(&far);
|
|
}
|
|
|
|
/* Independent quadrature of the KS coordinate-time transfer for a camera
|
|
* outside the worldtube, used to check the analytic primitive. */
|
|
static double simpson(const double a, const double b, int panels,
|
|
double (*f)(double, const void *), const void *ctx) {
|
|
if (panels < 2)
|
|
panels = 2;
|
|
if (panels % 2)
|
|
++panels;
|
|
const double h = (b - a) / panels;
|
|
double sum = f(a, ctx) + f(b, ctx);
|
|
for (int i = 1; i < panels; ++i)
|
|
sum += (i % 2 ? 4.0 : 2.0) * f(a + i * h, ctx);
|
|
return sum * h / 3.0;
|
|
}
|
|
|
|
typedef struct {
|
|
double beta;
|
|
} TransferContext;
|
|
|
|
static double transfer_dt(double r, const void *context) {
|
|
const TransferContext *c = context;
|
|
const double Q = 1.0 - c->beta * c->beta * (1.0 - 2.0 / r) / (r * r);
|
|
return 1.0 / ((1.0 - 2.0 / r) * sqrt(Q)) + 2.0 / (r - 2.0);
|
|
}
|
|
|
|
static double transfer_dphi(double r, const void *context) {
|
|
const TransferContext *c = context;
|
|
const double Q = 1.0 - c->beta * c->beta * (1.0 - 2.0 / r) / (r * r);
|
|
return c->beta / (r * r * sqrt(Q));
|
|
}
|
|
|
|
static void test_preroute_entry(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
const ObserverCamera camera = {.look_ra_deg = 0.0, .look_dec_deg = 0.0};
|
|
ObserverCamera positioned = camera;
|
|
positioned.position[0] = 500.0;
|
|
positioned.look_ra_deg = 180.0;
|
|
positioned.look_dec_deg = 0.0;
|
|
const double direction[3] = {cos(0.3), sin(0.3), 0.0};
|
|
MetricData metric;
|
|
CHECK(spacetime_eval(&source, 0.0, positioned.position, &metric) == 0,
|
|
"camera metric");
|
|
ObserverState observer;
|
|
CHECK(observer_from_coordinate_camera(&metric, &positioned, &observer,
|
|
NULL) == OBSERVER_BUILD_OK,
|
|
"camera observer");
|
|
|
|
MetricSlab *camera_slab = NULL;
|
|
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &camera_slab) == 0,
|
|
"camera slab");
|
|
GeodesicRayState camera_state;
|
|
CHECK(geodesic_initialize_past_ray(camera_slab, &observer, direction,
|
|
&camera_state) == 0,
|
|
"camera state");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
|
|
SchwarzschildCanonical camera_can;
|
|
CHECK(asymptotic_schwarzschild_canonical_from_state(
|
|
&end, &metric, 0.0, camera_state.x, camera_state.Pi,
|
|
camera_state.log_alpha_p0, &camera_can) == 0,
|
|
"camera canonical");
|
|
spacetime_free_slab(camera_slab);
|
|
|
|
AsymptoticRoute route;
|
|
CHECK(asymptotic_route_camera(&source, &observer, direction, &route) ==
|
|
ASYMPTOTIC_OK &&
|
|
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
|
|
"outside camera enters");
|
|
double value;
|
|
CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t,
|
|
route.x, &value) == 0 &&
|
|
fabs(value) < 1e-3,
|
|
"entry on worldtube");
|
|
|
|
MetricData entry_metric;
|
|
CHECK(spacetime_eval(&source, route.activate_t, route.x, &entry_metric) ==
|
|
0,
|
|
"entry metric");
|
|
SchwarzschildCanonical entry_can;
|
|
CHECK(asymptotic_schwarzschild_canonical_from_state(
|
|
&end, &entry_metric, route.activate_t, route.x, route.Pi,
|
|
route.log_alpha_p0, &entry_can) == 0,
|
|
"entry canonical");
|
|
CHECK(fabs(entry_can.beta - camera_can.beta) <
|
|
1e-12 * fmax(1.0, camera_can.beta),
|
|
"entry conserves impact parameter");
|
|
CHECK(fabs(entry_can.energy - camera_can.energy) < 1e-12,
|
|
"entry conserves energy");
|
|
CHECK(entry_can.radial_sign == -1, "entry is past-inward");
|
|
CHECK(route.activate_t < 0.0, "entry time is in the past");
|
|
|
|
const TransferContext context = {.beta = camera_can.beta};
|
|
const double t_analytic = -route.activate_t;
|
|
const double t_numeric =
|
|
simpson(256.0, 500.0, 20000, transfer_dt, &context);
|
|
CHECK(fabs(t_analytic - t_numeric) < 1e-9 * fmax(1.0, t_numeric),
|
|
"entry time matches quadrature");
|
|
const double dphi_numeric =
|
|
simpson(256.0, 500.0, 20000, transfer_dphi, &context);
|
|
const double dphi_entry = angle_between(camera_can.rhat, entry_can.rhat);
|
|
CHECK(fabs(dphi_entry - dphi_numeric) < 1e-9,
|
|
"entry azimuth matches quadrature");
|
|
if (fabs(t_analytic - t_numeric) >= 1e-9 * fmax(1.0, t_numeric) ||
|
|
fabs(dphi_entry - dphi_numeric) >= 1e-9)
|
|
fprintf(stderr, " beta=%.6g t_an=%.12g t_num=%.12g dphi_an=%.12g "
|
|
"dphi_num=%.12g\n",
|
|
camera_can.beta, t_analytic, t_numeric, dphi_entry,
|
|
dphi_numeric);
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
/* High-precision (mpmath, 60 digits) reference values fixed into the ordinary
|
|
* C test: radial, complex-pair, three-real, grazing, and large-radius angle
|
|
* cases. */
|
|
static void test_phi_reference_constants(void) {
|
|
static const struct {
|
|
double rho, beta, value;
|
|
} cases[] = {
|
|
{256.0, 0.0, 0.0},
|
|
{256.0, 5.0, 0.019532484697919191145},
|
|
{256.0, 60.0, 0.23656231243306290715},
|
|
{64.0, 64.0, 1.4199914058161304301},
|
|
{256.0, 255.0, 1.4527184167466732533},
|
|
{1.0e6, 1.0, 1.0000000000001666664e-6},
|
|
{300.0, 3.0, 0.010000165840750676787},
|
|
{100.0, 5.3, 0.053024471018799209953},
|
|
};
|
|
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
|
const double got =
|
|
asymptotic_schwarzschild_phi(cases[i].rho, cases[i].beta);
|
|
CHECK(fabs(got - cases[i].value) < 2e-13, "phi high-precision reference");
|
|
}
|
|
}
|
|
|
|
/* High-precision (mpmath, 60 digits) finish references covering radial,
|
|
* complex-pair, three-real, grazing, and large-radius scattering. The
|
|
* acceptance standard here is the error-budget-driven 1e-8 rad, not the
|
|
* measured ~1e-13. */
|
|
static void test_finish_reference_constants(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
|
|
static const struct {
|
|
double rho, beta, n[3];
|
|
} cases[] = {
|
|
{256.0, 0.0, {0.8, 0.6, 0.0}},
|
|
{256.0, 3.0, {0.80697631554502468, 0.59058380112341107, 0.0}},
|
|
{256.0, 60.0, {0.91833674808504193, 0.39579997109220491, 0.0}},
|
|
{256.0, 255.0, {0.69006511550899122, -0.72374728763399356, 0.0}},
|
|
{1.0e6, 1.0, {0.8000005999996, 0.5999991999997, 0.0}},
|
|
};
|
|
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
|
SchwarzschildCanonical canonical = {.end_id = 0,
|
|
.t = 0.0,
|
|
.rho = cases[i].rho,
|
|
.rhat = {0.8, 0.6, 0.0},
|
|
.Lhat = {0.0, 0.0, 1.0},
|
|
.beta = cases[i].beta,
|
|
.energy = 2.5,
|
|
.radial_sign = 1};
|
|
double n_inf[3], frequency = 0.0;
|
|
CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_inf,
|
|
&frequency) == 0,
|
|
"finish reference runs");
|
|
CHECK(angle_between(n_inf, cases[i].n) < 1e-8,
|
|
"finish n_inf high-precision reference");
|
|
CHECK(fabs(frequency - 0.4) < 1e-10 * 0.4,
|
|
"finish frequency high-precision reference");
|
|
}
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
/* Turning equation residual |Q| at the computed turning radius. The final
|
|
* scattering direction is validated by test_grazing_reference(). */
|
|
static void test_turning_reference(void) {
|
|
const double betas[] = {3.0 * sqrt(3.0) + 1e-9, 5.5, 6.0, 10.0,
|
|
60.0, 255.0, 3890.44};
|
|
for (size_t i = 0; i < sizeof betas / sizeof betas[0]; ++i) {
|
|
const double rho = asymptotic_schwarzschild_turning_rho(betas[i]);
|
|
CHECK(isfinite(rho) && rho > 3.0, "turning radius exists and is exterior");
|
|
const double Q =
|
|
1.0 - betas[i] * betas[i] * (1.0 - 2.0 / rho) / (rho * rho);
|
|
CHECK(fabs(Q) <= 1e-11, "turning equation residual");
|
|
}
|
|
}
|
|
|
|
/* High-precision entry coordinate-time and swept-azimuth references, checking
|
|
* both the KS time transfer and the entry direction construction. */
|
|
static void test_time_reference(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
|
|
static const struct {
|
|
double rho_cam, beta, time, dphi;
|
|
} cases[] = {
|
|
{500.0, 10.0, 246.7884398934447041137, 0.01907105306677434549856},
|
|
{500.0, 0.3, 246.693149021326385798, 0.0005718752307574524347376},
|
|
{256.5, 10.0, 0.5082474340167056157528, 0.00007620281793853560952548},
|
|
{256.5, 0.3, 0.5078666185420216617125, 0.000002284358278417004222584},
|
|
{1000.0, 50.0, 753.1528987272233278083, 0.1465476883815797019938},
|
|
{1.0e6, 10.0, 999777.308033789182372,
|
|
0.03906238263856681534781},
|
|
};
|
|
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
|
SchwarzschildCanonical camera = {.end_id = 0,
|
|
.t = 0.0,
|
|
.rho = cases[i].rho_cam,
|
|
.rhat = {1.0, 0.0, 0.0},
|
|
.Lhat = {0.0, 0.0, 1.0},
|
|
.beta = cases[i].beta,
|
|
.energy = 1.0,
|
|
.radial_sign = -1};
|
|
SchwarzschildRouteKind kind = SCH_ROUTE_UNSUPPORTED;
|
|
double activate_t = 0.0, x[3], Pi[3], log_alpha_p0 = 0.0, n_inf[3],
|
|
frequency = 0.0;
|
|
CHECK(asymptotic_schwarzschild_preroute(
|
|
&end, 256.0, &camera, &kind, &activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ENTRY,
|
|
"reference pre-route entry");
|
|
/* Error-budget-driven mixed tolerance, well below one ODE step (0.1 M)
|
|
* and future metric cadence. */
|
|
const double time_tol = 1e-7 + 1e-11 * fabs(cases[i].time);
|
|
CHECK(fabs(-activate_t - cases[i].time) < time_tol,
|
|
"entry time high-precision reference");
|
|
const double radius =
|
|
sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
|
|
const double rhat[3] = {x[0] / radius, x[1] / radius, x[2] / radius};
|
|
CHECK(fabs(angle_between(camera.rhat, rhat) - cases[i].dphi) < 2e-11,
|
|
"entry azimuth high-precision reference");
|
|
}
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
/* Near-grazing references where the exterior integrals are most sensitive:
|
|
* the two sides of beta_R enter through different branches and the KS time
|
|
* integral has a near-singular endpoint. (A photon-sphere turning is not
|
|
* reachable from a camera outside R/M >= 64, so it is not tested here.) */
|
|
static void test_grazing_reference(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
|
|
const double beta_R = 256.0 / sqrt(1.0 - 2.0 / 256.0);
|
|
SchwarzschildCanonical hit = {.end_id = 0,
|
|
.t = 0.0,
|
|
.rho = 500.0,
|
|
.rhat = {1.0, 0.0, 0.0},
|
|
.Lhat = {0.0, 0.0, 1.0},
|
|
.beta = beta_R * (1.0 - 1e-12),
|
|
.energy = 1.0,
|
|
.radial_sign = -1};
|
|
SchwarzschildRouteKind kind;
|
|
double activate_t, x[3], Pi[3], log_alpha_p0, n_inf[3], frequency;
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &hit, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ENTRY,
|
|
"near-grazing inside enters");
|
|
const double dphi_ref = 1.0389037630217253661;
|
|
const double time_ref = 434.0116073725480308524;
|
|
const double radius = sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
|
|
const double rhat[3] = {x[0] / radius, x[1] / radius, x[2] / radius};
|
|
CHECK(fabs(angle_between(hit.rhat, rhat) - dphi_ref) < 1e-8,
|
|
"near-grazing entry azimuth");
|
|
CHECK(fabs(-activate_t - time_ref) < 1e-7 + 1e-11 * time_ref,
|
|
"near-grazing entry time");
|
|
|
|
SchwarzschildCanonical miss = hit;
|
|
miss.beta = beta_R * (1.0 + 1e-12);
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &miss, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ESCAPED,
|
|
"near-grazing outside misses");
|
|
const double n_ref[3] = {-0.86581533530640297059,
|
|
-0.50036367289028986187, 0.0};
|
|
CHECK(angle_between(n_inf, n_ref) < 1e-8, "near-grazing miss n_inf");
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
/* Deterministic coverage of the three pre-route branches: past-outward,
|
|
* past-inward hit, and past-inward miss (turn before the worldtube). */
|
|
static void test_preroute_branches(void) {
|
|
SpacetimeSource source = {0};
|
|
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 0,
|
|
"create schwarzschild");
|
|
SpacetimeAsymptoticEnd end;
|
|
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
|
|
const double beta_R = 256.0 / sqrt(1.0 - 2.0 / 256.0);
|
|
|
|
SchwarzschildCanonical base = {.end_id = 0,
|
|
.t = 0.0,
|
|
.rho = 500.0,
|
|
.rhat = {1.0, 0.0, 0.0},
|
|
.Lhat = {0.0, 0.0, 1.0},
|
|
.beta = 10.0,
|
|
.energy = 1.0,
|
|
.radial_sign = -1};
|
|
SchwarzschildRouteKind kind;
|
|
double activate_t, x[3], Pi[3], log_alpha_p0, n_inf[3], frequency;
|
|
const double outward_eps = 1e-12;
|
|
|
|
SchwarzschildCanonical outward = base;
|
|
outward.radial_sign = 1;
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &outward, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ESCAPED,
|
|
"past-outward branch escapes");
|
|
CHECK(fabs(sqrt(n_inf[0]*n_inf[0]+n_inf[1]*n_inf[1]+n_inf[2]*n_inf[2]) -
|
|
1.0) < outward_eps,
|
|
"outward n_inf is unit");
|
|
CHECK(fabs(frequency - 1.0) < 1e-12, "outward frequency");
|
|
|
|
SchwarzschildCanonical hit = base;
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &hit, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ENTRY,
|
|
"past-inward hit branch enters");
|
|
|
|
/* Genuine on-boundary tangent: rho = R, beta = beta_R (so Q = 0), zero
|
|
* radial past component. It must not enter. */
|
|
SchwarzschildCanonical tangent = base;
|
|
tangent.rho = 256.0;
|
|
tangent.beta = beta_R;
|
|
tangent.radial_sign = 0;
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &tangent, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ESCAPED,
|
|
"on-boundary tangent escapes");
|
|
|
|
SchwarzschildCanonical miss = base;
|
|
miss.beta = beta_R + 5.0;
|
|
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &miss, &kind,
|
|
&activate_t, x, Pi, &log_alpha_p0,
|
|
n_inf, &frequency) == 0 &&
|
|
kind == SCH_ROUTE_ESCAPED,
|
|
"past-inward miss branch escapes");
|
|
CHECK(fabs(sqrt(n_inf[0]*n_inf[0]+n_inf[1]*n_inf[1]+n_inf[2]*n_inf[2]) -
|
|
1.0) < outward_eps,
|
|
"miss n_inf is unit");
|
|
/* A turning ray is deflected away from the radial direction. */
|
|
CHECK(angle_between(n_inf, miss.rhat) > 1e-3,
|
|
"miss n_inf is deflected");
|
|
spacetime_destroy(&source);
|
|
}
|
|
|
|
int main(void) {
|
|
test_round_trip();
|
|
test_finish_matches_integration();
|
|
test_preroute_entry();
|
|
test_phi_reference_constants();
|
|
test_finish_reference_constants();
|
|
test_turning_reference();
|
|
test_time_reference();
|
|
test_grazing_reference();
|
|
test_preroute_branches();
|
|
if (failures == 0)
|
|
puts("asymptotic schwarzschild regression passed");
|
|
else
|
|
fprintf(stderr, "%d asymptotic schwarzschild failures\n", failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|