Alcubierre: support superluminal v_s

This commit is contained in:
wyj committed 2026-10-06 02:51:20 -04:00
1 parent 0a46a7095b
commit b3f1e837d5
8 files changed
+438 -160

No files matched your search

+2 -2
View File
@@ -36,10 +36,10 @@ make -j
With no explicit `SPACETIME` setting, this builds all supported spacetimes:
| Executable | Spacetime |
| --- | --- |
| --------------------------------- | --------------------------------------------------------- |
| `build/Release/minkowski_sky` | Flat Minkowski spacetime |
| `build/Release/schwarzschild_sky` | Analytic Schwarzschild in ingoing Kerr–Schild coordinates |
| `build/Release/alcubierre_sky` | Analytic moving Alcubierre warp bubble, `x_s(t)=v_s t` (no capture) |
| `build/Release/alcubierre_sky` | Analytic moving Alcubierre warp bubble, `x_s(t)=v_s t` |
To build only one:
-2
View File
@@ -1130,8 +1130,6 @@ residual、Chebyshev 表或解析主项;运行期不得建表。
各 trial 先确定可表示的目标时间,再用实际 `target-before.t` 推进状态、构造
dense interpolant 与提交时间;不得以舍入前的步长推进空间、舍入后的步长记时间。
worldtube 采样的坐标时间同样使用实际差值回推运动,不假设半步总是可表示。
Alcubierre 的默认时间范围由 `1.25*4*R_escape/(1-|v_s|)` 给出;固定步的步数
估算不能截短自适应路径的历史,也不能替代其独立资源上限。
- 初始 accepted 状态的 RHS/metric 失败直接报告具体 point reason,不缩步;后续 stage
的 `OUT_OF_DOMAIN/INVALID_METRIC` 可有界缩步重试;`TIME_UNAVAILABLE/INTERNAL_ERROR`
不靠无限缩步;最小步长或时间不可进导致失败时报告 `INCOMPLETE/INTEGRATION_ERROR`。
+71 -35
View File
@@ -846,8 +846,8 @@ static void print_help(const char *program) {
stdout);
#ifdef SPACETIME_ALCUBIERRE
fputs(
"\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no capture):\n"
" --alcubierre-vs V Constant bubble velocity v_s, |v_s| < 1 (default: 0.5)\n"
"\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no geometric capture, shared dark policy):\n"
" --alcubierre-vs V Constant bubble velocity v_s (any finite value, default: 0.5)\n"
" --alcubierre-radius R Bubble radius R > 0 (default: 5)\n"
" --alcubierre-sigma S Wall sharpness sigma > 0 (default: 1)\n"
" The escape radius R + 20/sigma is derived internally; the\n"
@@ -991,19 +991,57 @@ static double alcubierre_time_step(const Settings *s) {
return fmin(0.1, 0.05 / s->alcubierre_sigma);
}
/* Coordinate-time coverage a past-directed Alcubierre ray must be granted.
* A ray that is nearly comoving with the bubble separates from its center in
* the propagation direction at only ~1 - |v_s|, so crossing the ~4*escape
* domain takes up to ~4*escape/(1 - |v_s|), scaled by ALCUBIERRE_BUDGET_MARGIN
* for lingering in the wall. This is a physical budget derived from the bubble
* geometry and the asymptotic separation rate; it is independent of the chosen
* step size and of the accepted-step count. */
/* Coordinate-time resource allowance for a past-directed Alcubierre ray:
*
* B = margin * 4 * R_escape / max(|1 - |v_s||, exp(-D)), D = dark threshold.
*
* For a sub-luminal ray whose separation |1 - |v_s|| dominates exp(-D) this
* reduces to the historical margin*4*escape/(1-|v_s|) geometry budget
* (margin*4 == 5). For
* |v_s| -> 1, including exactly |v_s| = 1 and super-luminal |v_s| > 1, the
* exp(-D) floor is motivated by the center-comoving axial relation
* exp(-(L-L0)) = 1 - |v_s|*(1-f); it cuts off the separation scale at the
* finite dark threshold. This is a resource heuristic, not a universal bound
* for arbitrary cameras or directions. B is a resource allowance, not a
* guarantee that every ray escapes: a ray that needs more coordinate time ends
* as UNRESOLVED/BUDGET_EXHAUSTED, which the render-level publication gate
* handles. B is independent of the chosen step size and accepted-step count.
*
* Saturation rules:
* - exp(-D) may underflow to 0 for a large D; the max still selects the
* finite separation unless the separation itself is 0.
* - when the ordinary 5*R/denom is not finite and positive (denom == 0, or
* the division/scale overflows), fall back to a log-space evaluation
* log B = log(5) + log(R) - max(log(separation), -D), which never forms
* exp(D); clamp the log to [log(DBL_MIN), log(DBL_MAX/4)] and exponentiate.
* - the ordinary result is clamped to the same [DBL_MIN, DBL_MAX/4] range,
* reserving DBL_MAX/4 for the default 4x retry growth.
* - exp(D) is never evaluated. */
static double alcubierre_trace_time_budget(const Settings *s) {
const double escape =
spacetime_alcubierre_escape_radius(s->alcubierre_radius,
s->alcubierre_sigma);
const double separation = 1.0 - fabs(s->alcubierre_vs);
return ALCUBIERRE_BUDGET_MARGIN * 4.0 * escape / separation;
const double separation = fabs(1.0 - fabs(s->alcubierre_vs));
const double floor = exp(-s->dark_threshold); /* may underflow to 0 */
const double upper = DBL_MAX / 4.0;
if (!isfinite(escape) || escape <= 0.0)
return DBL_MIN;
const double denom = fmax(separation, floor);
if (denom > 0.0 && isfinite(denom)) {
/* Do not pre-scale 5*R (which could overflow); divide first. */
const double scaled = (escape / denom) * (ALCUBIERRE_BUDGET_MARGIN * 4.0);
if (isfinite(scaled) && scaled > 0.0)
return fmin(fmax(scaled, DBL_MIN), upper);
}
/* Extreme fallback: separation == 0 with exp(-D) underflowed to 0, or the
* ordinary arithmetic saturated. Evaluate in log space without exp(D). */
double log_budget = log(ALCUBIERRE_BUDGET_MARGIN * 4.0) + log(escape) -
fmax(log(separation), -s->dark_threshold);
if (!isfinite(log_budget))
log_budget = log(upper);
log_budget = fmin(log_budget, log(upper));
log_budget = fmax(log_budget, log(DBL_MIN));
return fmin(fmax(exp(log_budget), DBL_MIN), upper);
}
/* Legacy accepted-step estimate for the same coverage, used only by the
@@ -1190,10 +1228,13 @@ static GeodesicTraceConfig trace_config(const Settings *s) {
#elif defined(SPACETIME_ALCUBIERRE)
default_step = alcubierre_time_step(s);
{
/* Accepted-step resource estimate along the legacy fixed-step policy. It
* may be capped at ALCUBIERRE_MAX_TRACE_STEPS; the coordinate-time coverage
* below is what guarantees the physical budget, so a capped count never
* shortens the trusted history. */
/* Accepted-step resource estimate along the legacy fixed-step policy.
* `alcubierre_step_budget` may be +inf or overflow for a near-luminal or
* super-luminal bubble, so the estimate is only cast when it is finite and
* strictly below the cap; otherwise the cap is kept. A capped count is an
* independent resource allowance and never shortens the trusted history,
* which the coordinate-time coverage below defines. It does not guarantee
* escape for every ray. */
const double estimated_steps = alcubierre_step_budget(s);
unsigned int budget = ALCUBIERRE_MAX_TRACE_STEPS;
if (isfinite(estimated_steps) && estimated_steps < (double)budget)
@@ -2393,33 +2434,28 @@ int main(int argc, char **argv) {
if (spacetime_create_alcubierre(&spacetime, settings.alcubierre_vs,
settings.alcubierre_radius,
settings.alcubierre_sigma)) {
fputs("Could not create Alcubierre spacetime source; require |v_s| < 1, "
"R > 0, sigma > 0.\n", stderr);
fputs("Could not create Alcubierre spacetime source; require finite "
"v_s, R > 0, sigma > 0, and a derived escape radius beyond R.\n",
stderr);
return 1;
}
/* The fixed-step RK4 comparison path sizes its history from
* step * accepted-step count, so its domain guard still rejects an
* estimate that exceeds the cap. The adaptive path uses an independent
* coordinate-time budget and must not be rejected here: trustworthy quota
* exhaustion is reported as UNRESOLVED and handled by the publication
* gate, and explicit lookback/step overrides remain independent. */
* step * accepted-step count, so its derived default estimate is still
* bounded by the cap. An explicit --trace-max-steps is a user resource
* allowance and is never rejected here; the adaptive path uses an
* independent coordinate-time budget and is never rejected here either
* (trustworthy quota exhaustion is reported as UNRESOLVED and handled by
* the publication gate). */
if (settings.stepper == GEODESIC_STEPPER_RK4 &&
!settings.trace_max_steps_specified &&
alcubierre_step_budget(&settings) >
(double)ALCUBIERRE_MAX_TRACE_STEPS) {
/* The estimate has a V-shaped minimum at sigma = 0.5, where the step
* stops being capped: below it the 20/sigma term dominates (increase
* sigma helps), above it the step scales as 1/sigma (decrease sigma
* helps), and at exactly 0.5 neither direction improves anything. */
const char *sigma_advice = "";
if (settings.alcubierre_sigma > 0.5)
sigma_advice = "decrease --alcubierre-sigma, ";
else if (settings.alcubierre_sigma < 0.5)
sigma_advice = "increase --alcubierre-sigma, ";
fprintf(stderr,
"Fixed-step RK4 Alcubierre trace budget exceeds the %u-step "
"cap; decrease --alcubierre-radius, %sor move --alcubierre-vs "
"away from +/-1, or use the adaptive default.\n",
ALCUBIERRE_MAX_TRACE_STEPS, sigma_advice);
"Fixed-step RK4 Alcubierre default trace budget exceeds the "
"%u-step cap; decrease --alcubierre-radius, adjust "
"--dark-threshold or --alcubierre-sigma, or use the adaptive "
"default or an explicit --trace-max-steps.\n",
ALCUBIERRE_MAX_TRACE_STEPS);
spacetime_destroy(&spacetime);
return 2;
}
+4 -2
View File
@@ -122,8 +122,10 @@ int spacetime_create_default(SpacetimeSource *source);
int spacetime_create_minkowski(SpacetimeSource *source, double escape_radius);
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
double escape_radius);
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires
* |vs| < 1, R > 0, and sigma > 0. */
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires a
* finite vs, R > 0, and sigma > 0. Sub- and super-luminal |vs| are accepted;
* classify() only ever reports ACTIVE or ESCAPED, and the shared
* camera-relative dark policy may terminate a ray as DARK for any finite vs. */
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma);
/* Bubble-centered escape radius used by the Alcubierre backend; also lets
+3 -6
View File
@@ -107,11 +107,8 @@ static SpacetimePointStatus alcubierre_eval(const SpacetimeSource *source,
return SPACETIME_POINT_OK;
}
/* A warp bubble has no curvature singularity or horizon for |v_s| < 1, so
* rays are only ever ACTIVE or ESCAPED; the exotic matter that would source
* the bubble is treated as optically transparent. The escape sphere follows
* the bubble, so rays terminate only once the metric is flat to machine
* precision at their current location. */
/* The exotic matter that would source the bubble is treated as optically
* transparent. The escape sphere follows the moving bubble. */
static SpacetimeRayStatus alcubierre_classify(const SpacetimeSource *source,
double t, const double x[3]) {
const AlcubierreContext *context = source->context;
@@ -179,7 +176,7 @@ double spacetime_alcubierre_escape_radius(double radius, double sigma) {
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma) {
if (source == NULL || !isfinite(vs) || fabs(vs) >= 1.0 ||
if (source == NULL || !isfinite(vs) ||
!isfinite(radius) || radius <= 0.0 || !isfinite(sigma) || sigma <= 0.0)
return -1;
/* Reject parameter combinations whose derived domain overflows or does not
+219 -90
View File
@@ -11,6 +11,7 @@ fixed-step reference convergence check. Small CPU 16x8/32x16 scenes keep the
runtime short; physical comparisons use stored lens-map endpoints, not only the
rendered PNG.
"""
import math
import os
import re
import struct
@@ -139,6 +140,28 @@ def sum_rhs(vertices):
return sum(v[14] for v in vertices)
def alcubierre_budget(escape, vs, dark_threshold):
"""Reference implementation of the production Alcubierre time allowance:
B = 5*escape / max(|1-|v_s||, exp(-D)), with the DBL_MIN..DBL_MAX/4
saturation and the log-space fallback used when the ordinary division is
not finite and positive. exp(D) is never formed."""
sep = abs(1.0 - abs(vs))
floor = math.exp(-dark_threshold)
denom = max(sep, floor)
upper = sys.float_info.max / 4.0
if denom > 0.0 and math.isfinite(denom):
scaled = 5.0 * (escape / denom)
if math.isfinite(scaled) and scaled > 0.0:
return min(max(scaled, sys.float_info.min), upper)
log_sep = math.log(sep) if sep > 0.0 else -math.inf
log_budget = math.log(5.0) + math.log(escape) - max(log_sep, -dark_threshold)
if not math.isfinite(log_budget):
log_budget = math.log(upper)
log_budget = min(log_budget, math.log(upper))
log_budget = max(log_budget, math.log(sys.float_info.min))
return min(max(math.exp(log_budget), sys.float_info.min), upper)
with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
dir=str(TMP_ROOT)) as directory:
tmp = Path(directory)
@@ -435,111 +458,217 @@ with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
print(f'{backend}: adaptive CLI checks passed', flush=True)
# Alcubierre production-default smoke: the DP54 default policy must
# validate and the migrated lookback budget must actually cover the warp
# bubble feature (rays integrate and escape) instead of pre-routing all
# misses. No movie/thread sweep for this third backend.
# Alcubierre production policy: sub- and super-luminal velocities share one
# finite resource allowance B = 5*escape / max(|1-|v_s||, exp(-D)); the DP54
# default must validate, actually integrate the warp feature, and reach both
# the shared DARK terminal and escapes. All images are tiny 8x4/16x8.
alc = BUILD / 'alcubierre_sky'
if not alc.exists():
print('alcubierre: binary absent, skipping', flush=True)
else:
alc_help = run(alc, '--help').stdout
ext = 'png' if '.png' in alc_help else 'ppm'
alc_map = tmp / 'alcubierre_default.grlens'
alc_out = tmp / f'alcubierre_default.{ext}'
run(alc, '--alcubierre-vs', 0.3, '--alcubierre-radius', 1,
'--alcubierre-sigma', 1, '--catalog', 'assets/sky_grid_5deg.csv',
'--width', 16, '--height', 8, '--fov-deg', 80, '--exposure', 1e-3,
'--coarse-cell-pixels', 8, '--refine-max-level', 0,
'--psf-relative-tail', 1e-4, '--verbose', '--output', alc_out,
'--lens-map-output', alc_map)
_, _, alc_prov = map_provenance(alc_map)
assert alc_prov['integrator'] == 1, alc_prov
assert alc_prov['min_step'] <= alc_prov['coordinate_time_step'] \
<= alc_prov['max_step']
assert alc_prov['max_lookback_time'] > 0
alc_vertices, _ = map_vertices(alc_map)
outcomes = {v[10] for v in alc_vertices}
assert 3 not in outcomes, f'Alcubierre default left INCOMPLETE rays'
assert any(v[10] == 0 for v in alc_vertices), \
'Alcubierre default produced no escaped ray'
assert sum_rhs(alc_vertices) > 0, \
'Alcubierre default pre-routed every ray; lookback misses feature'
assert image_payload(alc_out)
assert '|v_s| < 1' not in alc_help, 'help still claims |v_s| < 1'
escape1 = 1.0 + 20.0 / 1.0 # R = sigma = 1
# The migrated coordinate-time coverage is the physical geometry budget
# margin*4*escape/(1-|v_s|) with escape = R + 20/sigma, and it is
# independent of the accepted-step count and of the chosen initial
# step. Two cheap maps with different resource overrides must keep the
# same lookback.
alc_escape = 1.0 + 20.0 / 1.0 # R + 20/sigma for R=sigma=1
alc_sep = 1.0 - abs(0.3)
alc_expected_lookback = 1.25 * 4.0 * alc_escape / alc_sep
assert abs(alc_prov['max_lookback_time'] - alc_expected_lookback) \
< 1e-12, alc_prov['max_lookback_time']
def alc_camera(vs, ra_deg, dec_deg=0.0):
return ['--observer-position', '0', '0', '0',
'--observer-velocity', repr(vs), '0', '0',
'--look-ra-deg', repr(ra_deg),
'--look-dec-deg', repr(dec_deg)]
def alc_map_with(tag, *options):
m = tmp / f'alcubierre_{tag}.grlens'
run(alc, '--alcubierre-vs', 0.3, '--alcubierre-radius', 1,
'--alcubierre-sigma', 1, '--catalog', 'assets/sky_grid_5deg.csv',
'--width', 16, '--height', 8, '--fov-deg', 80, '--exposure',
1e-3, '--coarse-cell-pixels', 8, '--refine-max-level', 0,
'--psf-relative-tail', 1e-4, '--allow-incomplete',
'--output', tmp / f'alcubierre_{tag}.{ext}',
'--lens-map-output', m, *options)
return map_provenance(m)[2]
def alc_map_prov(tag, vs, D=8.0, radius=1.0, extra=(), allow=True,
width=8, height=4, cell=4, camera=None,
catalog=True):
m = tmp / f'alc_{tag}.grlens'
args = ['--alcubierre-vs', repr(vs), '--alcubierre-radius',
repr(radius), '--alcubierre-sigma', '1',
'--dark-threshold', repr(D), '--width', str(width),
'--height', str(height), '--fov-deg', 80, '--exposure',
'1e-3', '--coarse-cell-pixels', str(cell),
'--refine-max-level', 0, '--psf-relative-tail', 1e-4]
if catalog:
args += ['--catalog', 'assets/sky_grid_5deg.csv']
if camera is not None:
args += camera
if allow:
args.append('--allow-incomplete')
args += ['--output', str(tmp / f'alc_{tag}.{ext}'),
'--lens-map-output', str(m), *extra]
run(alc, *args)
return map_provenance(m)[2], m
steps_override = alc_map_with('steps_override', '--trace-max-steps', 8)
assert steps_override['initial_max_steps'] == 8
assert abs(steps_override['max_lookback_time'] - alc_expected_lookback) \
< 1e-12, steps_override
step_override = alc_map_with('step_override', '--ode-initial-step', 0.02)
assert abs(step_override['coordinate_time_step'] - 0.02) < 1e-15
assert abs(step_override['max_lookback_time'] - alc_expected_lookback) \
< 1e-12, step_override
# 1) Ordinary sub-luminal separation: B is exactly 5*escape/sep and is
# independent of the step count and of the initial step.
p_sub, _ = alc_map_prov('sub', 0.3, camera=alc_camera(0.3, 0.0))
assert p_sub['integrator'] == 1, p_sub
assert math.isclose(p_sub['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
assert p_sub['min_step'] <= p_sub['coordinate_time_step'] \
<= p_sub['max_step']
p_steps, _ = alc_map_prov('sub_steps', 0.3,
extra=('--trace-max-steps', '8'),
camera=alc_camera(0.3, 0.0))
assert p_steps['initial_max_steps'] == 8
assert math.isclose(p_steps['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
p_step, _ = alc_map_prov('sub_step', 0.3,
extra=('--ode-initial-step', '0.02'),
camera=alc_camera(0.3, 0.0))
assert abs(p_step['coordinate_time_step'] - 0.02) < 1e-15
assert math.isclose(p_step['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
# Extreme separation (v_s = 0.99999999) where the legacy fixed-step
# estimate far exceeds the cap. The DP path must accept an explicit
# tiny coordinate-time budget instead of being rejected at startup by
# the fixed-step guard, and it must actually exercise the quota path
# (UNRESOLVED rays or real RHS work), not pre-route everything to
# escapes. --allow-incomplete publishes the diagnostic frame.
extreme = ['--alcubierre-vs', 0.99999999, '--alcubierre-radius', 1,
'--alcubierre-sigma', 1, '--catalog',
'assets/sky_grid_5deg.csv', '--width', 8, '--height', 4,
'--fov-deg', 80, '--exposure', 1e-3,
'--coarse-cell-pixels', 4, '--refine-max-level', 0,
'--psf-relative-tail', 1e-4,
'--observer-position', 0, 0, 0,
'--observer-velocity', 0.99999999, 0, 0,
'--look-ra-deg', 0, '--look-dec-deg', 0]
ext_map = tmp / 'alcubierre_extreme.grlens'
# 2) Near-luminal (vs = 1, separation 0): the exp(-D) floor makes the
# allowance finite, and it grows with the dark threshold D.
p8, _ = alc_map_prov('near8', 1.0, D=8.0, camera=alc_camera(1.0, 0.0))
p12, _ = alc_map_prov('near12', 1.0, D=12.0, camera=alc_camera(1.0, 0.0))
assert math.isclose(p8['max_lookback_time'],
alcubierre_budget(escape1, 1.0, 8.0), rel_tol=1e-12)
assert math.isclose(p12['max_lookback_time'],
alcubierre_budget(escape1, 1.0, 12.0), rel_tol=1e-12)
assert p12['max_lookback_time'] > p8['max_lookback_time']
# 3) Both sides of the threshold-derived vcut and exactly luminal
# values use either separation or the finite floor; a tiny film traces
# and resolves without INCOMPLETE outcomes.
vcut = 1.0 - math.exp(-8.0)
for vs in (0.999, math.nextafter(vcut, 0.0),
math.nextafter(vcut, 1.0), 0.9999, math.nextafter(1.0, 0.0),
math.nextafter(1.0, 2.0)):
pv, mv = alc_map_prov(f'vcut_{vs!r}', vs, camera=alc_camera(vs, 0.0))
assert math.isclose(pv['max_lookback_time'],
alcubierre_budget(escape1, vs, 8.0),
rel_tol=1e-12), (vs, pv['max_lookback_time'])
vv, _ = map_vertices(mv)
assert 3 not in {v[10] for v in vv}, (vs, 'INCOMPLETE ray')
assert sum_rhs(vv) > 0, vs
# 4) The default camera (0,0,15 for R=5) must work with a superluminal
# bubble: the generic radius-15 camera lies inside escape radius 25.
pd, md = alc_map_prov('defaultcam', 2.0, radius=5.0)
dv, _ = map_vertices(md)
assert 3 not in {v[10] for v in dv}, 'default camera left INCOMPLETE rays'
assert sum_rhs(dv) > 0
# 5) Real small images at the critical velocities: the direction along
# the bubble motion is the DARK direction (Pi_x = sign(vs)); the
# central vertex must be DARK and some edge ray must escape. The
# 16x8 film with a 4-pixel coarse cell places a vertex exactly at the
# image center.
for vs in (1.0, -1.0, 2.0, -2.0):
ra = 0.0 if vs < 0.0 else 180.0
pimg, mimg = alc_map_prov(f'img_{vs!r}', vs, width=16, height=8,
cell=4, camera=alc_camera(vs, ra))
verts, _ = map_vertices(mimg)
outcomes = {v[10] for v in verts}
assert 3 not in outcomes, (vs, 'INCOMPLETE outcome', outcomes)
assert 0 in outcomes and 1 in outcomes, (vs, outcomes)
assert sum_rhs(verts) > 0, vs
central = min(verts, key=lambda v: (v[0] - 8.0) ** 2
+ (v[1] - 4.0) ** 2)
assert central[10] == 1, \
(vs, 'central vertex is not DARK', central[0], central[1],
central[10])
# 6) Extreme tiny-budget quota path: an explicit 4-step, 1-time
# allowance from an inside camera must stop as UNRESOLVED, never as
# a fabricated escape from a trace that took no steps.
extreme = (alc_camera(0.99999999, 0.0)
+ ['--alcubierre-vs', '0.99999999', '--alcubierre-radius',
'1', '--alcubierre-sigma', '1', '--catalog',
'assets/sky_grid_5deg.csv', '--width', '8', '--height',
'4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4'])
ext_map = tmp / 'alc_extreme.grlens'
run(alc, *extreme, '--integrator', 'dp54',
'--trace-lookback-time', 1, '--trace-max-steps', 4,
'--max-total-steps', 4, '--retry-step-increment', 0,
'--max-total-lookback-time', 1, '--retry-lookback-increment', 0,
'--allow-incomplete', '--output',
tmp / f'alcubierre_extreme.{ext}', '--lens-map-output', ext_map)
'--trace-lookback-time', '1', '--trace-max-steps', '4',
'--max-total-steps', '4', '--retry-step-increment', '0',
'--max-total-lookback-time', '1', '--retry-lookback-increment', '0',
'--allow-incomplete', '--output', tmp / f'alc_extreme.{ext}',
'--lens-map-output', ext_map)
_, _, ext_prov = map_provenance(ext_map)
assert ext_prov['integrator'] == 1
assert abs(ext_prov['max_lookback_time'] - 1.0) < 1e-15, ext_prov
assert ext_prov['initial_max_steps'] == 4, ext_prov
ext_vertices, _ = map_vertices(ext_map)
assert any(v[10] == 2 for v in ext_vertices) or \
sum_rhs(ext_vertices) > 0, \
'extreme Alcubierre case did not exercise the quota path'
assert all(v[10] == 2 for v in ext_vertices), \
('a 1-time/4-step trace fabricated a non-UNRESOLVED outcome',
[v[10] for v in ext_vertices])
# The same parameters without explicit DP budgets keep the legacy
# fixed-step startup guard, which still rejects the estimated domain.
rk4_extreme = run(
alc, '--integrator', 'rk4', '--alcubierre-vs', 0.99999999,
'--alcubierre-radius', 1, '--alcubierre-sigma', 1, '--catalog',
'assets/sky_grid_5deg.csv', '--width', 8, '--height', 4,
'--fov-deg', 80, '--exposure', 1e-3, '--coarse-cell-pixels', 4,
'--refine-max-level', 0, '--psf-relative-tail', 1e-4,
'--output', tmp / f'alcubierre_rk4_extreme.{ext}', ok=False)
assert rk4_extreme.returncode == 2, rk4_extreme.stderr
assert 'cap' in rk4_extreme.stderr, rk4_extreme.stderr
# 7) Large dark threshold (D=1000) with vs=1: exp(-1000) underflows to
# 0 and the separation is exactly 0, so the log fallback saturates
# the default allowance to DBL_MAX/4. A 4-step/4-total cap with
# retry disabled keeps the trace short while the provenance records
# the non-masked saturated default. An explicit --trace-lookback
# overrides it without masking the independent step allowance.
psat, _ = alc_map_prov('sat1000', 1.0, D=1000.0,
extra=('--trace-max-steps', '4',
'--max-total-steps', '4',
'--retry-step-increment', '0'),
camera=alc_camera(1.0, 0.0))
assert math.isclose(psat['max_lookback_time'],
sys.float_info.max / 4.0, rel_tol=1e-12), \
psat['max_lookback_time']
assert psat['initial_max_steps'] == 4
psat_ov, _ = alc_map_prov('sat1000_ov', 1.0, D=1000.0,
extra=('--trace-lookback-time', '1',
'--trace-max-steps', '4',
'--max-total-steps', '4',
'--retry-step-increment', '0'),
camera=alc_camera(1.0, 0.0))
assert psat_ov['max_lookback_time'] == 1.0, psat_ov
assert psat_ov['initial_max_steps'] == 4, psat_ov
# Explicit time override must not mask the (default) step allowance.
pind, _ = alc_map_prov('indep', 1.0, D=8.0,
extra=('--trace-lookback-time', '1'),
camera=alc_camera(1.0, 0.0))
assert pind['max_lookback_time'] == 1.0, pind
assert pind['initial_max_steps'] > 1, pind
print('alcubierre: default DP config validated and produced escapes; '
'extreme DP quota path ok, RK4 guard still rejects', flush=True)
# 8) RK4 guard: a super-luminal default (vs=2, small estimate) is no
# longer rejected; the guard triggers only when the *derived default*
# estimate exceeds the cap (D=12, vs=1), and an explicit
# --trace-max-steps is a user allowance that bypasses it.
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '2',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4', '--allow-incomplete',
'--output', tmp / f'alc_rk4_v2.{ext}')
rk4_guard = run(
alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--dark-threshold', '12', '--catalog', 'assets/sky_grid_5deg.csv',
'--width', '8', '--height', '4', '--fov-deg', '80',
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
'--output', tmp / f'alc_rk4_guard.{ext}', ok=False)
assert rk4_guard.returncode == 2, rk4_guard.stderr
assert 'cap' in rk4_guard.stderr, rk4_guard.stderr
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--dark-threshold', '12', '--trace-max-steps', '4',
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4', '--allow-incomplete',
'--output', tmp / f'alc_rk4_expl.{ext}')
# 9) Impossible parameters stay clearly rejected.
for bad in (['--alcubierre-vs', 'nan'], ['--alcubierre-vs', 'inf'],
['--alcubierre-radius', '0'], ['--alcubierre-sigma', '0']):
bad_out = tmp / f'alc_bad.{ext}'
rejected = run(alc, *bad, '--catalog', 'assets/sky_grid_5deg.csv',
'--width', '8', '--height', '4', '--fov-deg', '80',
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
'--output', bad_out, ok=False)
assert rejected.returncode != 0, rejected.stderr
assert not bad_out.exists()
print('alcubierre: budget/provenance, vcut scans, vcut images with '
'central DARK, default camera, large-D saturation, explicit '
'overrides and RK4 guard all passed', flush=True)
+111 -2
View File
@@ -35,8 +35,24 @@ int main(void) {
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);
/* Sub- and super-luminal velocities are both accepted. A successful
* constructor installs a context, so each acceptance uses its own temporary
* source that is destroyed immediately; the shared `source` above is never
* overwritten with a second live context. */
{
SpacetimeSource luminal = {0};
CHECK(spacetime_create_alcubierre(&luminal, 1.0, radius, sigma) == 0);
spacetime_destroy(&luminal);
}
{
SpacetimeSource superluminal = {0};
CHECK(spacetime_create_alcubierre(&superluminal, -1.5, radius, sigma) == 0);
spacetime_destroy(&superluminal);
}
/* Non-finite velocities stay rejected. */
CHECK(spacetime_create_alcubierre(&source, NAN, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, INFINITY, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, -INFINITY, 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. */
@@ -322,6 +338,99 @@ int main(void) {
}
}
/* Production DP54 axial superluminal check. A comoving bubble-center camera
* at R = 1, sigma = 1 (escape radius 21) sees Pi_x = +-1 along the bubble
* axis. The shared camera-relative dark policy must fire at threshold 8 for
* the direction along the bubble motion and the opposite direction must
* escape. The vs = +-2 constants were computed independently with mpmath;
* this test has no dependency on any local experiment fixture. */
{
const double R1 = 1.0, sig1 = 1.0;
static const double vlist[] = {1.0, -1.0, 2.0, -2.0, 0.9999};
for (size_t k = 0; k < sizeof vlist / sizeof vlist[0]; ++k) {
const double v = vlist[k];
const double sgn = v > 0.0 ? 1.0 : -1.0;
SpacetimeSource fast = {0};
CHECK(spacetime_create_alcubierre(&fast, v, R1, sig1) == 0);
ObserverCamera cam = {.coordinate_time = 0.0,
.position = {0.0, 0.0, 0.0},
.velocity = {v, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0,
.roll_deg = 0.0};
MetricData m;
CHECK(eval(&fast, 0.0, cam.position, &m) == 0);
ObserverState o;
CHECK(observer_from_coordinate_camera(&m, &cam, &o, NULL) ==
OBSERVER_BUILD_OK);
/* A coordinate-static center camera is timelike only for |v| < 1. */
ObserverCamera stat = cam;
stat.velocity[0] = stat.velocity[1] = stat.velocity[2] = 0.0;
ObserverState so;
const int static_ok = observer_from_coordinate_camera(&m, &stat, &so,
NULL) ==
OBSERVER_BUILD_OK;
CHECK(static_ok == (fabs(v) < 1.0));
/* An outer static camera in the flat exterior is always legal. */
{
const double pos[3] = {26.0, 0.0, 0.0};
MetricData om;
ObserverCamera oc = {.coordinate_time = 0.0,
.position = {26.0, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0,
.roll_deg = 0.0};
CHECK(eval(&fast, 0.0, pos, &om) == 0);
ObserverState oo;
CHECK(observer_from_coordinate_camera(&om, &oc, &oo, NULL) ==
OBSERVER_BUILD_OK);
}
const GeodesicTraceConfig trace = {
.coordinate_time_step = 0.05,
.max_steps = 100000u,
.threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 8.0,
.policy_version = 1},
.stepper = GEODESIC_STEPPER_DP54,
.atol_x = 1e-9,
.atol_Pi = 1e-9,
.atol_L = 1e-9,
.rtol = 1e-9,
.min_step = 1e-12,
.max_step = 0.4,
.consecutive_rejection_limit = 32,
.max_lookback_time = 30000.0};
const double n_dark[3] = {-sgn, 0.0, 0.0};
const double n_esc[3] = {sgn, 0.0, 0.0};
GeodesicRayState idark, iesc;
CHECK(geodesic_initialize_past_ray_metric(&m, &o, n_dark, &idark) == 0);
CHECK(geodesic_initialize_past_ray_metric(&m, &o, n_esc, &iesc) == 0);
printf("alcubierre vs=%.6g dark Pi_x=%.17g escape Pi_x=%.17g\n", v,
idark.Pi[0], iesc.Pi[0]);
CHECK(sgn * idark.Pi[0] > 0.999 && sgn * idark.Pi[0] < 1.000000001);
CHECK(sgn * iesc.Pi[0] < -0.999 && sgn * iesc.Pi[0] > -1.000000001);
const RayEndpoint dark = geodesic_trace_past(&fast, &o, n_dark, &trace);
CHECK(dark.outcome == RAY_OUTCOME_DARK);
CHECK(isfinite(dark.stop_coordinate_time));
CHECK(fabs(dark.threshold_value - 8.0) < 1e-6);
CHECK(fabs((dark.final_log_alpha_p0 - dark.final_log_alpha_p0_0) - 8.0) <
1e-6);
if (fabs(v) == 2.0) {
const double q = dark.final_x[0] - v * dark.stop_coordinate_time;
CHECK(fabs(fabs(q) - 1.2181434100155241) < 1e-6);
CHECK(fabs(dark.stop_coordinate_time + 7.09915163394274) < 1e-6);
}
const RayEndpoint esc = geodesic_trace_past(&fast, &o, n_esc, &trace);
CHECK(esc.outcome == RAY_OUTCOME_ESCAPED);
CHECK(isfinite(esc.frequency_ratio) && esc.frequency_ratio > 0.0);
if (fabs(v) == 2.0)
CHECK(fabs(esc.frequency_ratio - 3.0) < 1e-6);
for (int i = 0; i < 3; ++i)
CHECK(isfinite(esc.n_infinity[i]));
spacetime_destroy(&fast);
}
}
spacetime_destroy(&source);
puts("alcubierre regression passed");
return 0;
+28 -21
View File
@@ -116,32 +116,39 @@ with the bubble center following the constant-velocity worldline
$$f(r) = \frac{\tanh(\sigma(r+R)) - \tanh(\sigma(r-R))}{2\tanh(\sigma R)},\qquad
r_s = \sqrt{(x-x_s)^2 + y^2 + z^2}.$$
The bubble therefore propagates through the coordinates, and the metric is
time-dependent: the renderer evaluates `f(r_s)` and its spatial derivatives at
each coordinate time, while the extrinsic curvature supplies the required
`d_t gamma` information to the 3+1 null-ray equations. The exotic matter that
would source the bubble is treated as optically transparent and there is no
horizon, so in practice rays are active or escaped: the bubble has no causal
boundary at which `L - L0` can diverge, and the shared dark policy is not
expected to trigger. This is why the backend requires a sub-luminal
`|v_s| < 1`; at or above `1` the metric develops an ergoregion/event horizon and
static observers cease to exist, which is outside the current scope.
The renderer evaluates the moving bubble's time-dependent metric along each
ray. The exotic matter sourcing the bubble is treated as optically transparent.
The shared dark policy terminates rays when `L - L0 >= T` (`T = 8` by default,
set with `--dark-threshold`); this finite threshold can also be reached at
sub-luminal bubble velocities.
| Option | Meaning / default |
| --- | --- |
| `--alcubierre-vs V` | Constant shift parameter, `|V| < 1` (default 0.5) |
| `--alcubierre-vs V` | Constant bubble velocity `v_s` (any finite value, default 0.5) |
| `--alcubierre-radius R` | Bubble radius `R > 0` (default 5) |
| `--alcubierre-sigma S` | Wall sharpness `S > 0` (default 1) |
`f` decays to zero past `r_s = R` over a transition width `~1/sigma`, so the
finite escape sphere is bubble-centered with radius `R + 20/sigma` and needs no
CLI option; it follows the moving bubble, so rays terminate only once the local
metric is flat to below double precision. The single-frame camera default is
`(0,0,15)` at `t = 0`, when the bubble is still at the origin; it must lie
inside the escape sphere, or the observer build fails with an explicit error.
The per-ray step budget scales with the escape radius and `1/(1-|v_s|)`, so
near-luminal `v_s` still lets grazing rays escape; combinations whose
worst-case budget would exceed the internal cap are rejected at startup.
A camera must be timelike with an orthonormal tetrad. Its coordinate velocity
`V = dx/dt` must satisfy `|V - v_s f e_x| < 1`. A static camera requires
`|v_s f| < 1`; at the bubble center, the comoving velocity `(v_s, 0, 0)` is
timelike even for super-luminal bubbles. Cameras may lie inside or outside the
escape sphere; exterior rays are routed to their first entry or to infinity.
The escape sphere follows the bubble with radius `R + 20/sigma`; escaping rays
continue through a Minkowski exterior. The single-frame camera defaults to
`(0,0,15)` at `t = 0`. The finite-radius truncation leaves a residual shift of
order `|v_s| e^{-40}`; accuracy at extremely large velocities is not guaranteed.
Per-ray coordinate-time coverage is a **resource allowance**
$$B = \frac{5\,R_\text{escape}}{\max\!\big(|1-|v_s||,\; e^{-T}\big)},\qquad
R_\text{escape} = R + \frac{20}{\sigma},$$
with `T = --dark-threshold`, clamped to `[DBL_MIN, DBL_MAX/4]`. This is not a
completion guarantee: quota exhaustion returns `UNRESOLVED/BUDGET_EXHAUSTED`.
`--trace-lookback-time` overrides `B` independently of `--trace-max-steps`.
RK4 rejects a derived default step estimate above its internal cap; an explicit
`--trace-max-steps` bypasses that check.
Lensing and frequency shifts come from the bubble wall. The configuration is
invariant under the isometry `(t, x) -> (t + T, x + v_s T)`, so observers
@@ -152,7 +159,7 @@ example:
make -j PSF_BACKEND=cpu SPACETIME=alcubierre backend
./build/Release/alcubierre_sky --catalog assets/sky_grid_5deg.csv \
--observer-radius 15 --look-ra-deg 90 --look-dec-deg -90 \
--alcubierre-vs 0.5 --alcubierre-radius 5 --alcubierre-sigma 1 \
--alcubierre-vs 1.5 --alcubierre-radius 1 --alcubierre-sigma 1 \
--width 640 --height 360 --fov-deg 60 --exposure 1 \
--coarse-cell-pixels 16 --refine-max-level 2 --psf-direct \
--output output/imgs/alcubierre_wall.png