Feat: Add analytic moving Alcubierre spacetime backend

Add a third analytic spacetime provider for the moving Alcubierre warp
bubble, x_s(t) = v_s*t with x_s(0) = 0.  The lab slices stay flat, so
alpha = 1, gamma_ij = delta_ij, beta^x = -v_s f(r_s), and K_ij follows
from the flat spatial metric; the time dependence enters through the
moving shape argument.  The exotic matter is treated as transparent, so
there is no capture: rays are only ACTIVE or ESCAPED, with a bubble-
centered escape radius R + 20/sigma.

Expose --alcubierre-vs, --alcubierre-radius, and --alcubierre-sigma
(|v_s| < 1).  Scale the per-ray step budget with the escape radius and
1/(1-|v_s|) so near-luminal grazing rays still escape, and reject
parameter combinations whose worst-case budget exceeds the cap.  Use a
cancellation-free shape formula for small sigma*R and reject derived
escape radii that overflow.

The regression test covers metric reconstruction, d_beta/K finite
differences, the translation isometry, small-sigma stability, the flat
limit, reflection symmetry, step convergence, and a near-luminal slow
ray.  build.md, usage.md, and README.md document the backend.
This commit is contained in:
wyj committed 2026-10-03 03:09:26 -04:00
1 parent 4e34780fa9
commit 3cbf5ba386
8 files changed
+688 -16

No files matched your search

+328
View File
@@ -0,0 +1,328 @@
#include "geodesic.h"
#include "observer.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
#define CHECK(condition) do { if (!(condition)) { \
fprintf(stderr, "alcubierre regression failed at line %d: %s\n", \
__LINE__, #condition); \
return 1; } } while (0)
static double shape(double r, double radius, double sigma) {
const double sr = sigma * r;
const double sR = sigma * radius;
return (tanh(sr + sR) - tanh(sr - sR)) / (2.0 * tanh(sR));
}
static double metric_g00(const MetricData *m) {
double g = -m->alpha * m->alpha;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
g += m->gamma[i][j] * m->beta[i] * m->beta[j];
return g;
}
static int eval(const SpacetimeSource *source, double t, const double x[3],
MetricData *metric) {
return spacetime_eval(source, t, x, metric);
}
int main(void) {
const double vs = 0.5, radius = 5.0, sigma = 1.0;
const double escape = spacetime_alcubierre_escape_radius(radius, sigma);
SpacetimeSource source = {0};
MetricData metric;
CHECK(spacetime_create_alcubierre(&source, vs, radius, sigma) == 0);
CHECK(spacetime_create_alcubierre(&source, 1.0, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, -1.5, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, 0.0, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, radius, 0.0) != 0);
/* A derived escape radius that overflows or does not exceed R is rejected. */
CHECK(spacetime_create_alcubierre(&source, vs, 1.0, DBL_MIN) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, DBL_MAX, 1.0) != 0);
/* At t = 0 the bubble is centered on the origin: f = 1, beta^x = -v_s,
* flat spatial metric, K = 0. */
CHECK(eval(&source, 0.0, (double[]){0, 0, 0}, &metric) == 0);
CHECK(metric.alpha == 1.0);
CHECK(fabs(metric.beta[0] + vs) < 1e-15);
CHECK(metric.beta[1] == 0.0 && metric.beta[2] == 0.0);
for (int i = 0; i < 3; ++i) {
CHECK(metric.d_alpha[i] == 0.0);
for (int j = 0; j < 3; ++j) {
CHECK(metric.gamma[i][j] == (i == j ? 1.0 : 0.0));
CHECK(metric.K[i][j] == 0.0);
for (int k = 0; k < 3; ++k)
CHECK(metric.d_gamma[i][j][k] == 0.0);
}
}
/* Exact translation symmetry of the moving metric:
* g(t, x, y, z) == g(0, x - v_s t, y, z) for the 3+1 data. */
{
const double samples[3][4] = {{-2.0, 2.0, 3.0, -1.5},
{4.0, -6.5, 1.0, 2.0},
{-1.0, 0.5, -0.25, 0.75}};
for (int s = 0; s < 3; ++s) {
const double t = samples[s][0];
double x[3] = {samples[s][1], samples[s][2], samples[s][3]};
double shifted[3] = {x[0] - vs * t, x[1], x[2]};
MetricData mt, m0;
CHECK(eval(&source, t, x, &mt) == 0);
CHECK(eval(&source, 0.0, shifted, &m0) == 0);
CHECK(fabs(mt.beta[0] - m0.beta[0]) < 1e-14);
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) {
CHECK(fabs(mt.d_beta[i][j] - m0.d_beta[i][j]) < 1e-13);
CHECK(fabs(mt.K[i][j] - m0.K[i][j]) < 1e-13);
}
}
}
/* A generic off-axis point: 3+1 data must reconstruct the literal metric
* ds^2 = -dt^2 + (dx - v_s f(r_s) dt)^2 + dy^2 + dz^2. */
const double t = -2.0;
const double x[3] = {2.0, 3.0, -1.5};
const double dx = x[0] - vs * t;
const double r = sqrt(dx * dx + x[1] * x[1] + x[2] * x[2]);
const double f = shape(r, radius, sigma);
CHECK(eval(&source, t, x, &metric) == 0);
CHECK(fabs(metric_g00(&metric) - (-1.0 + vs * vs * f * f)) < 1e-14);
for (int i = 0; i < 3; ++i) {
double beta_lower = 0.0;
for (int j = 0; j < 3; ++j)
beta_lower += metric.gamma[i][j] * metric.beta[j];
const double target = (i == 0) ? -vs * f : 0.0;
CHECK(fabs(metric.beta[i] - target) < 1e-14);
CHECK(fabs(beta_lower - target) < 1e-14);
CHECK(metric.d_alpha[i] == 0.0);
}
/* d_beta and K against central differences of beta at fixed t. The spatial
* metric is flat and constant in time, so K_ij =
* (d_i beta_j + d_j beta_i) / 2. */
{
const double h = 1e-5;
for (int direction = 0; direction < 3; ++direction) {
double xp[3] = {x[0], x[1], x[2]};
double xm[3] = {x[0], x[1], x[2]};
MetricData mp, mm;
xp[direction] += h;
xm[direction] -= h;
CHECK(eval(&source, t, xp, &mp) == 0);
CHECK(eval(&source, t, xm, &mm) == 0);
for (int j = 0; j < 3; ++j) {
const double finite_difference =
(mp.beta[j] - mm.beta[j]) / (2.0 * h);
CHECK(fabs(metric.d_beta[direction][j] - finite_difference) < 1e-6);
}
}
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) {
const double expected =
0.5 * (metric.d_beta[i][j] + metric.d_beta[j][i]);
CHECK(fabs(metric.K[i][j] - expected) < 1e-14);
CHECK(fabs(metric.K[i][j] - metric.K[j][i]) < 1e-15);
}
}
/* Shape and derivative across the whole sigma*R domain, including the tiny
* sigma*R regime where the direct tanh difference loses all its digits. A
* long-double cosh form is cancellation-free and serves as the reference. */
{
static const double cases[][2] = {
{1e-20, 1.0}, {1e-8, 0.5}, {1e-3, 2.0}, {0.1, 0.3},
{0.24, 1.0}, {0.26, 1.0}, {0.5, 0.5}, {1.0, 1.0},
{5.0, 5.0}, {5.0, 8.0}};
for (size_t k = 0; k < sizeof cases / sizeof cases[0]; ++k) {
const double sigma_r = cases[k][0];
const double sr = cases[k][1];
SpacetimeSource local = {0};
CHECK(spacetime_create_alcubierre(&local, vs, sigma_r, 1.0) == 0);
const double r = sr; /* sigma = 1, so R = sigma_r and r = sigma_r_test */
MetricData m;
CHECK(eval(&local, 0.0, (double[]){r, 0.0, 0.0}, &m) == 0);
const double f = -m.beta[0] / vs;
const double df = -m.d_beta[0][0] / vs;
const long double C = coshl(2.0L * (long double)sigma_r);
const long double fref =
(C + 1.0L) / (coshl(2.0L * (long double)r) + C);
const long double dfref =
-(C + 1.0L) * 2.0L * sinhl(2.0L * (long double)r) /
((coshl(2.0L * (long double)r) + C) *
(coshl(2.0L * (long double)r) + C));
CHECK(fabsl((long double)f - fref) < 1e-12L);
CHECK(fabsl((long double)df - dfref) < 1e-9L);
/* The escape sphere must be flat to below binary64 epsilon. */
const double local_escape =
spacetime_alcubierre_escape_radius(sigma_r, 1.0);
CHECK(eval(&local, 0.0, (double[]){local_escape, 0.0, 0.0}, &m) == 0);
CHECK(fabs(m.beta[0] / vs) < 1e-15);
spacetime_destroy(&local);
}
}
/* Classification follows the bubble and is never CAPTURED. */
CHECK(spacetime_classify(&source, 0.0, (double[]){0, 0, 0}) ==
SPACETIME_RAY_ACTIVE);
CHECK(spacetime_classify(&source, 0.0,
(double[]){escape - 0.5, 0, 0}) ==
SPACETIME_RAY_ACTIVE);
CHECK(spacetime_classify(&source, 0.0,
(double[]){escape + 1.0, 0, 0}) ==
SPACETIME_RAY_ESCAPED);
CHECK(spacetime_classify(&source, 0.0, (double[]){0, 0, 1000}) ==
SPACETIME_RAY_ESCAPED);
/* At t = 3 the bubble center is at x_s = 1.5; the sphere moves with it. */
CHECK(spacetime_classify(&source, 3.0, (double[]){vs * 3.0, 0, 0}) ==
SPACETIME_RAY_ACTIVE);
CHECK(spacetime_classify(&source, 3.0,
(double[]){vs * 3.0 + escape + 1.0, 0, 0}) ==
SPACETIME_RAY_ESCAPED);
/* Isometry check: the moving metric is invariant under the spacetime
* translation (t, x) -> (t + T, x + v_s T). Two static observers related by
* this isometry must therefore see identical escaping directions and
* frequency ratios. This exercises the x_s(t) time dependence end to end. */
{
const double T = 3.0;
ObserverCamera camera0 = {.coordinate_time = 0.0,
.position = {0.0, 0.0, 15.0},
.look_ra_deg = 90.0,
.look_dec_deg = -90.0};
ObserverCamera camera1 = {.coordinate_time = T,
.position = {vs * T, 0.0, 15.0},
.look_ra_deg = 90.0,
.look_dec_deg = -90.0};
MetricData m0, m1;
ObserverState o0, o1;
CHECK(eval(&source, camera0.coordinate_time, camera0.position, &m0) == 0);
CHECK(eval(&source, camera1.coordinate_time, camera1.position, &m1) == 0);
CHECK(observer_from_coordinate_camera(&m0, &camera0, &o0, NULL) ==
OBSERVER_BUILD_OK);
CHECK(observer_from_coordinate_camera(&m1, &camera1, &o1, NULL) ==
OBSERVER_BUILD_OK);
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.02,
.max_steps = 1u << 20};
const double directions[3][3] = {{1, 0, 0}, {1, 0.25, 0}, {1, 0, 0.3}};
for (int i = 0; i < 3; ++i) {
double n[3] = {directions[i][0], directions[i][1], directions[i][2]};
const double norm = sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
for (int k = 0; k < 3; ++k)
n[k] /= norm;
const RayEndpoint r0 = geodesic_trace_past(&source, &o0, n, &trace);
const RayEndpoint r1 = geodesic_trace_past(&source, &o1, n, &trace);
CHECK(r0.status == RAY_ENDPOINT_ESCAPED);
CHECK(r1.status == RAY_ENDPOINT_ESCAPED);
for (int k = 0; k < 3; ++k)
CHECK(fabs(r0.n_infinity[k] - r1.n_infinity[k]) < 1e-6);
CHECK(fabs(r0.frequency_ratio - r1.frequency_ratio) < 1e-6);
}
}
/* Flat limit v_s = 0 is exactly Minkowski. */
{
SpacetimeSource flat = {0};
CHECK(spacetime_create_alcubierre(&flat, 0.0, radius, sigma) == 0);
MetricData flat_metric;
CHECK(eval(&flat, 0.0, (double[]){2, 3, 4}, &flat_metric) == 0);
CHECK(flat_metric.alpha == 1.0);
CHECK(flat_metric.beta[0] == 0.0 && flat_metric.beta[1] == 0.0 &&
flat_metric.beta[2] == 0.0);
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.25,
.max_steps = 200};
const ObserverState observer = observer_fixed_at_origin();
const RayEndpoint ray = geodesic_trace_past(
&flat, &observer, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(fabs(ray.n_infinity[0]) < 1e-12);
CHECK(fabs(ray.n_infinity[1]) < 1e-12);
CHECK(fabs(ray.n_infinity[2] + 1.0) < 1e-12);
CHECK(fabs(ray.frequency_ratio - 1.0) < 1e-12);
spacetime_destroy(&flat);
}
/* Reflection symmetry at fixed t: invariant under y -> -y, so transverse
* beta derivatives and K components flip sign. */
{
MetricData mirrored;
CHECK(eval(&source, t, (double[]){x[0], -x[1], x[2]}, &mirrored) == 0);
CHECK(fabs(metric.beta[0] - mirrored.beta[0]) < 1e-15);
CHECK(fabs(metric.d_beta[0][0] - mirrored.d_beta[0][0]) < 1e-14);
CHECK(fabs(metric.d_beta[1][0] + mirrored.d_beta[1][0]) < 1e-14);
CHECK(fabs(metric.K[0][1] + mirrored.K[0][1]) < 1e-14);
CHECK(fabs(metric.K[0][0] - mirrored.K[0][0]) < 1e-14);
}
/* Near-luminal bubble: a photon that propagates along +x with the bubble
* separates from its center at only 1 - |v_s| and, traced backwards, meets
* the bubble again near t ~ -15/(1-v_s) = -15000. It must still reach the
* escape sphere; with a fixed 2^18 budget it would end in MAX_STEPS. The
* budget below is the one main.c derives: 1.25 * 4*escape/((1-|v_s|)*step)
* = 1.25 * 4*25/(0.001*0.05) = 2.5e6. */
{
SpacetimeSource fast = {0};
CHECK(spacetime_create_alcubierre(&fast, 0.999, radius, sigma) == 0);
ObserverCamera camera = {.position = {15.0, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0};
MetricData camera_metric;
ObserverState observer;
CHECK(eval(&fast, 0.0, camera.position, &camera_metric) == 0);
CHECK(observer_from_coordinate_camera(&camera_metric, &camera, &observer,
NULL) == OBSERVER_BUILD_OK);
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.05,
.max_steps = 2500000u};
const RayEndpoint ray = geodesic_trace_past(
&fast, &observer, (double[]){-1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
spacetime_destroy(&fast);
}
/* Refinement convergence: a ray grazing the bubble wall must converge in
* n_infinity as the coordinate step is halved. */
{
ObserverCamera camera = {.position = {-15.0, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0};
MetricData camera_metric;
ObserverState observer;
CHECK(eval(&source, 0.0, camera.position, &camera_metric) == 0);
CHECK(observer_from_coordinate_camera(&camera_metric, &camera, &observer,
NULL) == OBSERVER_BUILD_OK);
double n[3] = {0.9995, 0.0316, 0.0};
{
const double norm = sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
for (int k = 0; k < 3; ++k)
n[k] /= norm;
}
RayEndpoint previous = {0};
double previous_error = INFINITY;
for (int level = 0; level < 3; ++level) {
const GeodesicTraceConfig trace = {
.coordinate_time_step = 0.08 / (1 << level),
.max_steps = 1u << 20};
const RayEndpoint ray = geodesic_trace_past(&source, &observer, n, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
if (level > 0) {
double error = 0.0;
for (int k = 0; k < 3; ++k) {
const double difference = ray.n_infinity[k] - previous.n_infinity[k];
error += difference * difference;
}
error = sqrt(error);
CHECK(error <= previous_error);
previous_error = error;
}
if (level == 2)
CHECK(previous_error < 1e-5);
previous = ray;
}
}
spacetime_destroy(&source);
puts("alcubierre regression passed");
return 0;
}