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
+440 -162

No files matched your search

+4 -4
View File
@@ -35,11 +35,11 @@ make -j
With no explicit `SPACETIME` setting, this builds all supported spacetimes: With no explicit `SPACETIME` setting, this builds all supported spacetimes:
| Executable | Spacetime | | Executable | Spacetime |
| --- | --- | | --------------------------------- | --------------------------------------------------------- |
| `build/Release/minkowski_sky` | Flat Minkowski spacetime | | `build/Release/minkowski_sky` | Flat Minkowski spacetime |
| `build/Release/schwarzschild_sky` | Analytic Schwarzschild in ingoing Kerr–Schild coordinates | | `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: To build only one:
-2
View File
@@ -1130,8 +1130,6 @@ residual、Chebyshev 表或解析主项;运行期不得建表。
各 trial 先确定可表示的目标时间,再用实际 `target-before.t` 推进状态、构造 各 trial 先确定可表示的目标时间,再用实际 `target-before.t` 推进状态、构造
dense interpolant 与提交时间;不得以舍入前的步长推进空间、舍入后的步长记时间。 dense interpolant 与提交时间;不得以舍入前的步长推进空间、舍入后的步长记时间。
worldtube 采样的坐标时间同样使用实际差值回推运动,不假设半步总是可表示。 worldtube 采样的坐标时间同样使用实际差值回推运动,不假设半步总是可表示。
Alcubierre 的默认时间范围由 `1.25*4*R_escape/(1-|v_s|)` 给出;固定步的步数
估算不能截短自适应路径的历史,也不能替代其独立资源上限。
- 初始 accepted 状态的 RHS/metric 失败直接报告具体 point reason,不缩步;后续 stage - 初始 accepted 状态的 RHS/metric 失败直接报告具体 point reason,不缩步;后续 stage
的 `OUT_OF_DOMAIN/INVALID_METRIC` 可有界缩步重试;`TIME_UNAVAILABLE/INTERNAL_ERROR` 的 `OUT_OF_DOMAIN/INVALID_METRIC` 可有界缩步重试;`TIME_UNAVAILABLE/INTERNAL_ERROR`
不靠无限缩步;最小步长或时间不可进导致失败时报告 `INCOMPLETE/INTEGRATION_ERROR`。 不靠无限缩步;最小步长或时间不可进导致失败时报告 `INCOMPLETE/INTEGRATION_ERROR`。
+71 -35
View File
@@ -846,8 +846,8 @@ static void print_help(const char *program) {
stdout); stdout);
#ifdef SPACETIME_ALCUBIERRE #ifdef SPACETIME_ALCUBIERRE
fputs( fputs(
"\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no capture):\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, |v_s| < 1 (default: 0.5)\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-radius R Bubble radius R > 0 (default: 5)\n"
" --alcubierre-sigma S Wall sharpness sigma > 0 (default: 1)\n" " --alcubierre-sigma S Wall sharpness sigma > 0 (default: 1)\n"
" The escape radius R + 20/sigma is derived internally; the\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); return fmin(0.1, 0.05 / s->alcubierre_sigma);
} }
/* Coordinate-time coverage a past-directed Alcubierre ray must be granted. /* Coordinate-time resource allowance for a past-directed Alcubierre ray:
* 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 * B = margin * 4 * R_escape / max(|1 - |v_s||, exp(-D)), D = dark threshold.
* 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 * For a sub-luminal ray whose separation |1 - |v_s|| dominates exp(-D) this
* geometry and the asymptotic separation rate; it is independent of the chosen * reduces to the historical margin*4*escape/(1-|v_s|) geometry budget
* step size and of the accepted-step count. */ * (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) { static double alcubierre_trace_time_budget(const Settings *s) {
const double escape = const double escape =
spacetime_alcubierre_escape_radius(s->alcubierre_radius, spacetime_alcubierre_escape_radius(s->alcubierre_radius,
s->alcubierre_sigma); s->alcubierre_sigma);
const double separation = 1.0 - fabs(s->alcubierre_vs); const double separation = fabs(1.0 - fabs(s->alcubierre_vs));
return ALCUBIERRE_BUDGET_MARGIN * 4.0 * escape / separation; 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 /* 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) #elif defined(SPACETIME_ALCUBIERRE)
default_step = alcubierre_time_step(s); default_step = alcubierre_time_step(s);
{ {
/* Accepted-step resource estimate along the legacy fixed-step policy. It /* Accepted-step resource estimate along the legacy fixed-step policy.
* may be capped at ALCUBIERRE_MAX_TRACE_STEPS; the coordinate-time coverage * `alcubierre_step_budget` may be +inf or overflow for a near-luminal or
* below is what guarantees the physical budget, so a capped count never * super-luminal bubble, so the estimate is only cast when it is finite and
* shortens the trusted history. */ * 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); const double estimated_steps = alcubierre_step_budget(s);
unsigned int budget = ALCUBIERRE_MAX_TRACE_STEPS; unsigned int budget = ALCUBIERRE_MAX_TRACE_STEPS;
if (isfinite(estimated_steps) && estimated_steps < (double)budget) 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, if (spacetime_create_alcubierre(&spacetime, settings.alcubierre_vs,
settings.alcubierre_radius, settings.alcubierre_radius,
settings.alcubierre_sigma)) { settings.alcubierre_sigma)) {
fputs("Could not create Alcubierre spacetime source; require |v_s| < 1, " fputs("Could not create Alcubierre spacetime source; require finite "
"R > 0, sigma > 0.\n", stderr); "v_s, R > 0, sigma > 0, and a derived escape radius beyond R.\n",
stderr);
return 1; return 1;
} }
/* The fixed-step RK4 comparison path sizes its history from /* The fixed-step RK4 comparison path sizes its history from
* step * accepted-step count, so its domain guard still rejects an * step * accepted-step count, so its derived default estimate is still
* estimate that exceeds the cap. The adaptive path uses an independent * bounded by the cap. An explicit --trace-max-steps is a user resource
* coordinate-time budget and must not be rejected here: trustworthy quota * allowance and is never rejected here; the adaptive path uses an
* exhaustion is reported as UNRESOLVED and handled by the publication * independent coordinate-time budget and is never rejected here either
* gate, and explicit lookback/step overrides remain independent. */ * (trustworthy quota exhaustion is reported as UNRESOLVED and handled by
* the publication gate). */
if (settings.stepper == GEODESIC_STEPPER_RK4 && if (settings.stepper == GEODESIC_STEPPER_RK4 &&
!settings.trace_max_steps_specified &&
alcubierre_step_budget(&settings) > alcubierre_step_budget(&settings) >
(double)ALCUBIERRE_MAX_TRACE_STEPS) { (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, fprintf(stderr,
"Fixed-step RK4 Alcubierre trace budget exceeds the %u-step " "Fixed-step RK4 Alcubierre default trace budget exceeds the "
"cap; decrease --alcubierre-radius, %sor move --alcubierre-vs " "%u-step cap; decrease --alcubierre-radius, adjust "
"away from +/-1, or use the adaptive default.\n", "--dark-threshold or --alcubierre-sigma, or use the adaptive "
ALCUBIERRE_MAX_TRACE_STEPS, sigma_advice); "default or an explicit --trace-max-steps.\n",
ALCUBIERRE_MAX_TRACE_STEPS);
spacetime_destroy(&spacetime); spacetime_destroy(&spacetime);
return 2; 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_minkowski(SpacetimeSource *source, double escape_radius);
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass, int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
double escape_radius); double escape_radius);
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires /* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires a
* |vs| < 1, R > 0, and sigma > 0. */ * 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, int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma); double radius, double sigma);
/* Bubble-centered escape radius used by the Alcubierre backend; also lets /* 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; return SPACETIME_POINT_OK;
} }
/* A warp bubble has no curvature singularity or horizon for |v_s| < 1, so /* The exotic matter that would source the bubble is treated as optically
* rays are only ever ACTIVE or ESCAPED; the exotic matter that would source * transparent. The escape sphere follows the moving bubble. */
* 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. */
static SpacetimeRayStatus alcubierre_classify(const SpacetimeSource *source, static SpacetimeRayStatus alcubierre_classify(const SpacetimeSource *source,
double t, const double x[3]) { double t, const double x[3]) {
const AlcubierreContext *context = source->context; 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, int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma) { 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) !isfinite(radius) || radius <= 0.0 || !isfinite(sigma) || sigma <= 0.0)
return -1; return -1;
/* Reject parameter combinations whose derived domain overflows or does not /* 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 runtime short; physical comparisons use stored lens-map endpoints, not only the
rendered PNG. rendered PNG.
""" """
import math
import os import os
import re import re
import struct import struct
@@ -139,6 +140,28 @@ def sum_rhs(vertices):
return sum(v[14] for v in 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-', with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
dir=str(TMP_ROOT)) as directory: dir=str(TMP_ROOT)) as directory:
tmp = Path(directory) tmp = Path(directory)
@@ -435,111 +458,217 @@ with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
print(f'{backend}: adaptive CLI checks passed', flush=True) print(f'{backend}: adaptive CLI checks passed', flush=True)
# Alcubierre production-default smoke: the DP54 default policy must # Alcubierre production policy: sub- and super-luminal velocities share one
# validate and the migrated lookback budget must actually cover the warp # finite resource allowance B = 5*escape / max(|1-|v_s||, exp(-D)); the DP54
# bubble feature (rays integrate and escape) instead of pre-routing all # default must validate, actually integrate the warp feature, and reach both
# misses. No movie/thread sweep for this third backend. # the shared DARK terminal and escapes. All images are tiny 8x4/16x8.
alc = BUILD / 'alcubierre_sky' alc = BUILD / 'alcubierre_sky'
if not alc.exists(): if not alc.exists():
print('alcubierre: binary absent, skipping', flush=True) print('alcubierre: binary absent, skipping', flush=True)
else: else:
alc_help = run(alc, '--help').stdout alc_help = run(alc, '--help').stdout
ext = 'png' if '.png' in alc_help else 'ppm' ext = 'png' if '.png' in alc_help else 'ppm'
alc_map = tmp / 'alcubierre_default.grlens' assert '|v_s| < 1' not in alc_help, 'help still claims |v_s| < 1'
alc_out = tmp / f'alcubierre_default.{ext}' escape1 = 1.0 + 20.0 / 1.0 # R = sigma = 1
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)
# The migrated coordinate-time coverage is the physical geometry budget def alc_camera(vs, ra_deg, dec_deg=0.0):
# margin*4*escape/(1-|v_s|) with escape = R + 20/sigma, and it is return ['--observer-position', '0', '0', '0',
# independent of the accepted-step count and of the chosen initial '--observer-velocity', repr(vs), '0', '0',
# step. Two cheap maps with different resource overrides must keep the '--look-ra-deg', repr(ra_deg),
# same lookback. '--look-dec-deg', repr(dec_deg)]
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_map_with(tag, *options): def alc_map_prov(tag, vs, D=8.0, radius=1.0, extra=(), allow=True,
m = tmp / f'alcubierre_{tag}.grlens' width=8, height=4, cell=4, camera=None,
run(alc, '--alcubierre-vs', 0.3, '--alcubierre-radius', 1, catalog=True):
'--alcubierre-sigma', 1, '--catalog', 'assets/sky_grid_5deg.csv', m = tmp / f'alc_{tag}.grlens'
'--width', 16, '--height', 8, '--fov-deg', 80, '--exposure', args = ['--alcubierre-vs', repr(vs), '--alcubierre-radius',
1e-3, '--coarse-cell-pixels', 8, '--refine-max-level', 0, repr(radius), '--alcubierre-sigma', '1',
'--psf-relative-tail', 1e-4, '--allow-incomplete', '--dark-threshold', repr(D), '--width', str(width),
'--output', tmp / f'alcubierre_{tag}.{ext}', '--height', str(height), '--fov-deg', 80, '--exposure',
'--lens-map-output', m, *options) '1e-3', '--coarse-cell-pixels', str(cell),
return map_provenance(m)[2] '--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) # 1) Ordinary sub-luminal separation: B is exactly 5*escape/sep and is
assert steps_override['initial_max_steps'] == 8 # independent of the step count and of the initial step.
assert abs(steps_override['max_lookback_time'] - alc_expected_lookback) \ p_sub, _ = alc_map_prov('sub', 0.3, camera=alc_camera(0.3, 0.0))
< 1e-12, steps_override assert p_sub['integrator'] == 1, p_sub
step_override = alc_map_with('step_override', '--ode-initial-step', 0.02) assert math.isclose(p_sub['max_lookback_time'],
assert abs(step_override['coordinate_time_step'] - 0.02) < 1e-15 alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
assert abs(step_override['max_lookback_time'] - alc_expected_lookback) \ assert p_sub['min_step'] <= p_sub['coordinate_time_step'] \
< 1e-12, step_override <= 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 # 2) Near-luminal (vs = 1, separation 0): the exp(-D) floor makes the
# estimate far exceeds the cap. The DP path must accept an explicit # allowance finite, and it grows with the dark threshold D.
# tiny coordinate-time budget instead of being rejected at startup by p8, _ = alc_map_prov('near8', 1.0, D=8.0, camera=alc_camera(1.0, 0.0))
# the fixed-step guard, and it must actually exercise the quota path p12, _ = alc_map_prov('near12', 1.0, D=12.0, camera=alc_camera(1.0, 0.0))
# (UNRESOLVED rays or real RHS work), not pre-route everything to assert math.isclose(p8['max_lookback_time'],
# escapes. --allow-incomplete publishes the diagnostic frame. alcubierre_budget(escape1, 1.0, 8.0), rel_tol=1e-12)
extreme = ['--alcubierre-vs', 0.99999999, '--alcubierre-radius', 1, assert math.isclose(p12['max_lookback_time'],
'--alcubierre-sigma', 1, '--catalog', alcubierre_budget(escape1, 1.0, 12.0), rel_tol=1e-12)
'assets/sky_grid_5deg.csv', '--width', 8, '--height', 4, assert p12['max_lookback_time'] > p8['max_lookback_time']
'--fov-deg', 80, '--exposure', 1e-3,
'--coarse-cell-pixels', 4, '--refine-max-level', 0, # 3) Both sides of the threshold-derived vcut and exactly luminal
'--psf-relative-tail', 1e-4, # values use either separation or the finite floor; a tiny film traces
'--observer-position', 0, 0, 0, # and resolves without INCOMPLETE outcomes.
'--observer-velocity', 0.99999999, 0, 0, vcut = 1.0 - math.exp(-8.0)
'--look-ra-deg', 0, '--look-dec-deg', 0] for vs in (0.999, math.nextafter(vcut, 0.0),
ext_map = tmp / 'alcubierre_extreme.grlens' 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', run(alc, *extreme, '--integrator', 'dp54',
'--trace-lookback-time', 1, '--trace-max-steps', 4, '--trace-lookback-time', '1', '--trace-max-steps', '4',
'--max-total-steps', 4, '--retry-step-increment', 0, '--max-total-steps', '4', '--retry-step-increment', '0',
'--max-total-lookback-time', 1, '--retry-lookback-increment', 0, '--max-total-lookback-time', '1', '--retry-lookback-increment', '0',
'--allow-incomplete', '--output', '--allow-incomplete', '--output', tmp / f'alc_extreme.{ext}',
tmp / f'alcubierre_extreme.{ext}', '--lens-map-output', ext_map) '--lens-map-output', ext_map)
_, _, ext_prov = map_provenance(ext_map) _, _, ext_prov = map_provenance(ext_map)
assert ext_prov['integrator'] == 1 assert ext_prov['integrator'] == 1
assert abs(ext_prov['max_lookback_time'] - 1.0) < 1e-15, ext_prov assert abs(ext_prov['max_lookback_time'] - 1.0) < 1e-15, ext_prov
assert ext_prov['initial_max_steps'] == 4, ext_prov assert ext_prov['initial_max_steps'] == 4, ext_prov
ext_vertices, _ = map_vertices(ext_map) ext_vertices, _ = map_vertices(ext_map)
assert any(v[10] == 2 for v in ext_vertices) or \ assert all(v[10] == 2 for v in ext_vertices), \
sum_rhs(ext_vertices) > 0, \ ('a 1-time/4-step trace fabricated a non-UNRESOLVED outcome',
'extreme Alcubierre case did not exercise the quota path' [v[10] for v in ext_vertices])
# The same parameters without explicit DP budgets keep the legacy # 7) Large dark threshold (D=1000) with vs=1: exp(-1000) underflows to
# fixed-step startup guard, which still rejects the estimated domain. # 0 and the separation is exactly 0, so the log fallback saturates
rk4_extreme = run( # the default allowance to DBL_MAX/4. A 4-step/4-total cap with
alc, '--integrator', 'rk4', '--alcubierre-vs', 0.99999999, # retry disabled keeps the trace short while the provenance records
'--alcubierre-radius', 1, '--alcubierre-sigma', 1, '--catalog', # the non-masked saturated default. An explicit --trace-lookback
'assets/sky_grid_5deg.csv', '--width', 8, '--height', 4, # overrides it without masking the independent step allowance.
'--fov-deg', 80, '--exposure', 1e-3, '--coarse-cell-pixels', 4, psat, _ = alc_map_prov('sat1000', 1.0, D=1000.0,
'--refine-max-level', 0, '--psf-relative-tail', 1e-4, extra=('--trace-max-steps', '4',
'--output', tmp / f'alcubierre_rk4_extreme.{ext}', ok=False) '--max-total-steps', '4',
assert rk4_extreme.returncode == 2, rk4_extreme.stderr '--retry-step-increment', '0'),
assert 'cap' in rk4_extreme.stderr, rk4_extreme.stderr 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; ' # 8) RK4 guard: a super-luminal default (vs=2, small estimate) is no
'extreme DP quota path ok, RK4 guard still rejects', flush=True) # 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}; SpacetimeSource source = {0};
MetricData metric; MetricData metric;
CHECK(spacetime_create_alcubierre(&source, vs, radius, sigma) == 0); CHECK(spacetime_create_alcubierre(&source, vs, radius, sigma) == 0);
CHECK(spacetime_create_alcubierre(&source, 1.0, radius, sigma) != 0); /* Sub- and super-luminal velocities are both accepted. A successful
CHECK(spacetime_create_alcubierre(&source, -1.5, radius, sigma) != 0); * 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, 0.0, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, radius, 0.0) != 0); CHECK(spacetime_create_alcubierre(&source, vs, radius, 0.0) != 0);
/* A derived escape radius that overflows or does not exceed R is rejected. */ /* 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); spacetime_destroy(&source);
puts("alcubierre regression passed"); puts("alcubierre regression passed");
return 0; 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 $$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}.$$ r_s = \sqrt{(x-x_s)^2 + y^2 + z^2}.$$
The bubble therefore propagates through the coordinates, and the metric is The renderer evaluates the moving bubble's time-dependent metric along each
time-dependent: the renderer evaluates `f(r_s)` and its spatial derivatives at ray. The exotic matter sourcing the bubble is treated as optically transparent.
each coordinate time, while the extrinsic curvature supplies the required The shared dark policy terminates rays when `L - L0 >= T` (`T = 8` by default,
`d_t gamma` information to the 3+1 null-ray equations. The exotic matter that set with `--dark-threshold`); this finite threshold can also be reached at
would source the bubble is treated as optically transparent and there is no sub-luminal bubble velocities.
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.
| Option | Meaning / default | | 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-radius R` | Bubble radius `R > 0` (default 5) |
| `--alcubierre-sigma S` | Wall sharpness `S > 0` (default 1) | | `--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 A camera must be timelike with an orthonormal tetrad. Its coordinate velocity
finite escape sphere is bubble-centered with radius `R + 20/sigma` and needs no `V = dx/dt` must satisfy `|V - v_s f e_x| < 1`. A static camera requires
CLI option; it follows the moving bubble, so rays terminate only once the local `|v_s f| < 1`; at the bubble center, the comoving velocity `(v_s, 0, 0)` is
metric is flat to below double precision. The single-frame camera default is timelike even for super-luminal bubbles. Cameras may lie inside or outside the
`(0,0,15)` at `t = 0`, when the bubble is still at the origin; it must lie escape sphere; exterior rays are routed to their first entry or to infinity.
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 The escape sphere follows the bubble with radius `R + 20/sigma`; escaping rays
near-luminal `v_s` still lets grazing rays escape; combinations whose continue through a Minkowski exterior. The single-frame camera defaults to
worst-case budget would exceed the internal cap are rejected at startup. `(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 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 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 make -j PSF_BACKEND=cpu SPACETIME=alcubierre backend
./build/Release/alcubierre_sky --catalog assets/sky_grid_5deg.csv \ ./build/Release/alcubierre_sky --catalog assets/sky_grid_5deg.csv \
--observer-radius 15 --look-ra-deg 90 --look-dec-deg -90 \ --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 \ --width 640 --height 360 --fov-deg 60 --exposure 1 \
--coarse-cell-pixels 16 --refine-max-level 2 --psf-direct \ --coarse-cell-pixels 16 --refine-max-level 2 --psf-direct \
--output output/imgs/alcubierre_wall.png --output output/imgs/alcubierre_wall.png