diff --git a/Makefile b/Makefile index 480992f..033aea5 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,7 @@ endif # built for a different backend. TEST_OUT_DIR := $(OBJECT_DIR)/$(HDR_BUILD_TAG) TEST_TARGET := $(TEST_OUT_DIR)/test_geodesic +ADAPTIVE_GEODESIC_TEST_TARGET := $(TEST_OUT_DIR)/test_geodesic_adaptive ASYMPTOTIC_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic_schwarzschild TERMINATION_ORACLE_TEST_TARGET := $(TEST_OUT_DIR)/test_termination_oracle @@ -207,6 +208,12 @@ $(TEST_OUT_DIR): | $(BUILD_DIR) $(TEST_TARGET): tests/test_geodesic.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR) $(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@ +# Explicit adaptive DP5(4) core and RayPool regression. The test textually +# includes both analytic providers (renaming spacetime_create_default), so the +# link line deliberately omits src/spacetime_{minkowski,schwarzschild}.c. +$(ADAPTIVE_GEODESIC_TEST_TARGET): tests/test_geodesic_adaptive.c $(COMMON_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR) + $(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -DGEODESIC_EVENT_TESTING -Isrc $^ $(LDLIBS) -o $@ + $(ASYMPTOTIC_TEST_TARGET): tests/test_asymptotic.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR) $(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@ @@ -273,10 +280,11 @@ FAST_PSF_FFTW_TEST_DEP := FAST_PSF_FFTW_TEST_RUN := endif -test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET) $(TERMINATION_ORACLE_TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) $(ALCUBIERRE_TEST_TARGET) $(OBSERVER_TRACK_TEST_TARGET) $(CATALOG_PREFETCH_TEST_TARGET) $(FAST_PSF_FFTW_TEST_DEP) $(TONE_MAP_TEST_TARGET) $(MOVIE_OUTPUT_TEST_TARGET) $(SENSOR_BLOOM_TEST_TARGET) +test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ADAPTIVE_GEODESIC_TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET) $(TERMINATION_ORACLE_TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) $(ALCUBIERRE_TEST_TARGET) $(OBSERVER_TRACK_TEST_TARGET) $(CATALOG_PREFETCH_TEST_TARGET) $(FAST_PSF_FFTW_TEST_DEP) $(TONE_MAP_TEST_TARGET) $(MOVIE_OUTPUT_TEST_TARGET) $(SENSOR_BLOOM_TEST_TARGET) $(TEST_OUT_DIR)/test_observer_minkowski $(TEST_OUT_DIR)/test_observer_schwarzschild $(TEST_TARGET) + $(ADAPTIVE_GEODESIC_TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET) $(TERMINATION_ORACLE_TEST_TARGET) @@ -290,6 +298,7 @@ test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOT $(MOVIE_OUTPUT_TEST_TARGET) $(SENSOR_BLOOM_TEST_TARGET) python3 tests/test_camera_cli.py $(BUILD_DIR) $(TEST_OUT_DIR) + python3 tests/test_adaptive_cli.py $(BUILD_DIR) $(TEST_OUT_DIR) tone-map-test: $(TONE_MAP_TEST_TARGET) $(TONE_MAP_TEST_TARGET) diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/README.md b/benchmarks/adaptive_step_bounds_2026-10-05/README.md new file mode 100644 index 0000000..5dfef4c --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/README.md @@ -0,0 +1,224 @@ +# adaptive_step_bounds_2026-10-05 + +Long-term benchmark for calibrating the DP54 adaptive-integrator step bounds +(`min_step` / `max_step`) of the past-directed null-geodesic tracer, and the +public record of the one authorised 4K Schwarzschild `max_step` 2-vs-8 pair. + +The benchmark runs the public endpoint `geodesic_trace_past()`, the production +DP core, and bounded adaptive-mesh renders against the analytic +Minkowski / Schwarzschild / Alcubierre backends. It does not modify production +sources. + +## Build (required before renderer scripts) + +The Python mesh/4K drivers start from an existing renderer binary. The shell +endpoint/core drivers compile their own small harnesses against production code. + +``` +make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend +# binary: build/Release/schwarzschild_sky +``` + +## Files + +| file | role | +|---|---| +| `a_public_endpoints.c` | Experiment A: step-bound scans through the public endpoint, tol=1e-12 reference, RK4 h-halving on near-critical rays. | +| `b_actual_h.c` | Experiment B: textually includes `src/geodesic.c`, drives the production `dp_advance_one` on finite trusted intervals to measure observed accepted `h`. | +| `critical_ref_check.c` | Critical-classification probe (r30/r100 inward, r2.1 outward; `theta_crit` and `theta_crit±1e-7`; tol/max_step ladder). | +| `mesh_maps.py` | Pure GRLENS v3 `load`/`compare`; no renderer, no untracked import. | +| `run_mesh.py` | 8 bounded 320x180 candidates (R100 and R2.1, `max_step` .5/2/8/32). | +| `run_mesh_reference.py` | 2 tight-tolerance references and comparisons against the 8 maps. | +| `summarize_4k.py` | Postprocess existing maps only; never invokes a renderer. | +| `run_4k_pair.py` | One-task, at-most-two-attempt 4K pair; refuses to run without `--authorize-two-4k`. | +| `record_environment.py` | Provenance (git/hashes/compiler/cpu/build/run commands); no render. | +| `failure_floor.c` | Failure-path floor diagnostic; includes `../../tests/test_geodesic_adaptive.c`. | +| `run_failure_floor.sh` | Builds/runs `failure_floor.c` with `-DGEODESIC_EVENT_TESTING`. | +| `run_limited.sh` | Builds/runs Experiment A + B; all output under `OUT_DIR`. | +| `run_critical_ref.sh` | Builds/runs `critical_ref_check.c`; all output under `OUT_DIR`. | +| `summarize.py` | `OUT_DIR` CLI arg (default `local/adaptive_step_bounds_2026-10-05`); writes `OUT_DIR/logs/summary.txt`. | +| `results/` | Tracked long-term evidence (see below). | + +## Reproduction + +``` +AB_OUT="$PWD/local/adaptive_step_bounds_2026-10-05" +OUT="$PWD/local/adaptive_bounds_mesh" + +bash benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh "$AB_OUT" +bash benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh "$AB_OUT" +python3 benchmarks/adaptive_step_bounds_2026-10-05/summarize.py "$AB_OUT" + +python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py "$OUT" +python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py "$OUT" +bash benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh "$OUT" + +# 4K only after explicit fresh user authorization for that task: +python3 benchmarks/adaptive_step_bounds_2026-10-05/run_4k_pair.py \ + --authorize-two-4k --out local/adaptive_bounds_4k +``` + +Defaults: A/B `OUT_DIR` is `local/adaptive_step_bounds_2026-10-05`; mesh +`OUT_DIR` is `local/adaptive_bounds_mesh`; 4K `--out` default is +`local/adaptive_bounds_4k`. All logs/CSV go to `OUT_DIR`; the tracked +benchmark directory holds only sources and `results/`. Scratch binaries go to +`/tmp/opencode/step_bounds/`. + +The `run_4k_pair.py` driver refuses to run without `--authorize-two-4k`, keeps +an attempt ledger, and has no automatic retry. Authorisation does not extend to +later tasks or routine tests. + +All catalog inputs are generated inline by the scripts (column header +`longitude_deg,latitude_deg,temperature_K,amplitude`, one 1e-30 star); no +external survey CSV is read. + +## Results + +### 1. Public-endpoint step-bound scan (Experiment A/B) + +Environment and full table inputs: `results/source_environment.txt`, +`results/summary_tables.txt`; full stdout: `results/expA.log`, +`results/expB.log`. Per-ray raw CSV (not tracked) is reproduced by +`run_limited.sh`. + +| backend | worst near-critical sky error vs tol=1e-12 ref | notes | +|---|---|---| +| Schwarzschild r30 | 2.82e-4 rad (`max_step`≥1); 8.67e-5 rad at 0.25 | r100 8.0e-5 / 2.6e-5; r2.1 2.87e-4 | +| Minkowski | Δn = 0, Δg/g = 0 (analytic) | crossing x/t ≤ 7.7e-11 at `max_step`≤256 | +| Alcubierre | ≤1.8e-10 rad, no class change | 8x vs 1x `initial` RHS cost 4.3x–5.7x | + +- Dark terminal `L−L0` margin agrees with the reference to ≤2.1e-13 in every + config; near-critical dark stop times differ up to ~41 time units between + tol=1e-9 and tol=1e-12 (orbit-count sensitivity, reported not hidden). +- Schwarzschild floor plateau: `min_step` `1e-14 … 1e-2` give bit-identical + results and RHS; only `0.1` breaks one r1.5 ray + (`INCOMPLETE/INTEGRATION_ERROR`). No evidence to raise the default floor, + and no evidence that `1e-12` is uniquely optimal (it is never active). +- Observed accepted `h` (Experiment B, finite window `T=min(20, ref span)`): + **Schwarzschild only** — r1.5 radial 0.0921, all other sampled Schwarzschild + rays ≥0.1; Alcubierre minimum = its initial step (0.05 / 0.005 / 0.0005); + Minkowski 1.0. This is a finite-window diagnostic, **not** a full-trajectory + minimum. +- RK4 h-halving worst (r2.1, `theta_crit−1e-7`): 1.04e-5 rad. + +### 2. Bounded 320x180 adaptive mesh + +Numeric inputs: `results/mesh_summary.json`, +`results/mesh_reference_summary.json`. Scene: R100/FOV45/jacobian0.2 and +R2.1/outward/FOV90; DP54, hmin 1e-12, initial 0.1, tol 1e-9 (tight reference +1e-12), 8 threads, one-point dim catalog. + +| scene | `max_step` | persistent vertices | RHS | wall (s) | +|---|---:|---:|---:|---:| +| R100 | 0.5 | 3158 | 16813349 | 2.071 | +| R100 | 2 | 3158 | 6030185 | 0.979 | +| R100 | 8 | 3158 | 4230835 | 0.823 | +| R100 | 32 | 3158 | 4081609 | 0.830 | +| R2.1 | 0.5 | 5496 | 20136508 | 1.974 | +| R2.1 | 2 | 5496 | 9548434 | 1.178 | +| R2.1 | 8 | 5496 | 7389760 | 0.970 | +| R2.1 | 32 | 5496 | 7197806 | 0.972 | + +- 2→8 RHS reduction 29.84% (R100) / 22.61% (R2.1); 8→32 only 3.53% / 2.60%. +- Identical terminal classifications and mesh/sample counts for all four caps. +- Error vs tight reference: R100 max sky 9.216e-8 (hmax2) / 8.744e-8 (hmax8), + mutual 2-vs-8 difference 7.063e-9 rad; R2.1 max sky 1.4772e-5 rad at both, + mutual difference 8.370e-11 rad. The larger R2.1 reference error is + tolerance/conditioning sensitive and is not cured by a smaller `max_step`. +- Max log-frequency differences ≤8.51e-10 (R100) / ≤2.10e-9 (R2.1). +- Short single-run wall times include setup/cache/output; not a timing study. + +### 3. One authorised 4K pair (R100, 3840x2160, FOV45) + +Raw terminal output (byte-for-byte): `results/4k_hmax2.log`, +`results/4k_hmax8.log`; provenance: `results/4k_metadata.json`, +`results/4k_attempts.json`; postprocessed: `results/4k_final_summary.json`, +`results/4k_raw_summary.log`. + +| metric | `max_step`=2 | `max_step`=8 | +|---|---:|---:| +| persistent vertices / triangles | 66045 / 131338 | 66045 / 131338 | +| outcomes | 62419 ESC, 3626 DARK | 62419 ESC, 3626 DARK | +| unresolved / error | 0 | 0 | +| persistent accepted | 15,768,677 | 10,183,020 | +| persistent rejected | 97,176 | 194,781 | +| persistent RHS | 130,409,139 | 91,991,144 | +| trace+refine time (s) | 10.774 | 7.805 | +| process wall (s) | 11.3893 | 8.38925 | + +- Triangle payload is bit-identical; shared film identities 66045, terminal + mismatches 0; max mutual endpoint sky difference 7.9544e-7 rad, max logg + difference 3.4711e-11. +- Persistent-vertex RHS reduction −29.46%; accepted −35.42%; wall −26.34%. +- **Cost scope:** the v3 map stores final persistent vertices only. The + requested-sample count including discarded refinement probes is 116308 + (> 66045), so discarded probes contribute no stored cost and these sums must + not be read as full-render executed RHS totals. +- **Timing:** one sequential pair only; no statistical timing confidence. +- PSF splat cached/direct = 0 and `--psf-min-y` discarded 1965 (hmax2) / 1954 + (hmax8) events; this does not measure image error. The PNGs are dark + diagnostic backgrounds; no 2MASS catalog was used. + +### 4. Critical-classification reference check + +Full output: `results/critical_ref.log`, `results/critical_ref.csv` (36 +endpoint traces). The exact `theta_crit` direction is a classification +separatrix; it is reported, never asserted. + +- r2.1 `theta_crit`: ref tol1e-12/h0.25 vs tol1e-13/h0.125 sky difference + **2.33638 rad** — the exactly-critical escape direction is not + independently converged. `EXACT_CRITICAL_REFERENCE_NOT_CONVERGED`. +- r30 `theta_crit` (DARK) stop-time difference 13.056; r100 `theta_crit` (DARK) + 12.700. At tol1e-11, h2-vs-h8 at r2.1 `theta_crit` is 5.09e-5 rad. +- Neighbours `±1e-7`: both reference tiers agree (same outcome/reason/end, + clean, positive steps) at every neighbour. Escaped-neighbour sky differences + between the layers are ~1e-7 rad (9.47e-8 r30, 2.74e-8 r100, 1.42e-7 r2.1), + consistent with the loose 1e-4 magnitude trend; DARK neighbours report + stop-time differences 1.5e-7 … 7.5e-7. +- `CRITICAL_REFERENCE_CHECK status=0` (no neighbour reference disagreement or + error endpoint). + +## Decision summary + +- **Schwarzschild `max_step = 8`**: supported by the far-field cost plateau + (2→8 saves ~23–30% persistent RHS; 8→32 only ~3%) together with the tight + reference and 4K 2-vs-8 comparisons. This is not claimed as globally optimal + and gives no guarantee for arbitrary near-critical/shadow directions. +- **`min_step = 1e-12`**: retained as a non-binding numerical guard; not a + measured optimum. +- **Minkowski `max_step = 16`** and **Alcubierre `max_step = 8 × initial`**: + retained as conservative caps. Their bounded accuracy checks support keeping + these values, not claiming cost optimality; larger Alcubierre caps still + reduce RHS, but have larger frequency errors in some cases. +- The R2.1 exactly-critical difference is tolerance-sensitive; + `critical_ref_check.c` confirms the exact direction is not + independently converged and the report does not treat it as certified. + +## Caveats + +- Analytic backends only; no time-dependent numerical-relativity metric was + exercised. Bounds and the `unit = 1` calibration are not transferable to NR. +- Experiment B samples `T = min(20, reference span)`; no full-trajectory + minimum-step claim is made. +- The 4K numbers are one sequential pair; no timing-stability confidence and + no image-error bound (inverse-lens-map Jacobian amplification is separate). +- Sky-angle differences divided by pixel angle are not inverse-lens-map image + error bounds. +- `failure_floor.c` reuses the regression invalid-metric temporal-domain + fixture; it is not a physical shadow or floor calibration. + +## Verification of the selected defaults and reproduction tools + +`make -B -j4 BUILD_TYPE=Debug test` passed after selecting the Schwarzschild +cap of 8 and adding CLI provenance assertions for the default bounds. Full +terminal output is retained locally at +`local/step_bounds_mesh/final_debug_tests.log` (`STEP_BOUNDS_FULL_DEBUG_EXIT=0`). +The explicit-RK4 HDR reference tests remain bit-identical. The Release +Schwarzschild binary was rebuilt with the selected default. + +The self-contained limited driver was also executed with the relative output +path `local/step_bounds_reproduction`: 1500 endpoint traces and 2427 observed +finite-window steps completed, including summary generation. Small angular +differences use `atan2(|cross|,dot)` rather than rounded `acos(dot)`. Running the +4K driver without fresh explicit authorization refuses with exit code 2; +this check launches no renderer. No further 4K attempt was made. diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/a_public_endpoints.c b/benchmarks/adaptive_step_bounds_2026-10-05/a_public_endpoints.c new file mode 100644 index 0000000..6567a37 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/a_public_endpoints.c @@ -0,0 +1,867 @@ +/* + * Experiment A: DP54 step-bound scan through the *public* production + * endpoint geodesic_trace_past(). + * + * Sub-commands: + * schwarzschild : r30/r100 static inward, r2.1 static outward, r1.5 free-fall + * minkowski : moving observer, escape sphere 64, analytic flat check + * alcubierre : comoving bubble-center camera, vs .3/.9, sigma 1/10/100 + * + * Each sub-command computes one DP54 tol=1e-12 reference per ray, then scans + * - upper scan : max_step over a list, min_step fixed + * - min scan : min_step over a list, max_step fixed + * and compares every result against the reference (class, sky angle, g, + * dark threshold margin / stop time, cost). Raw per-ray CSV and stdout + * summaries are produced. No production source is modified. + * + * Link line (do NOT link geodesic.c twice; the backends are textually included + * below, so only the common sources are linked): + * cc -std=c11 -O2 -Isrc a_public_endpoints.c geodesic.c asymptotic.c \ + * asymptotic_schwarzschild.c spacetime_common.c observer.c -lm + */ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include + +/* Textually include all three analytic backends with renamed default + * constructors so one binary can exercise every provider (the files each + * define spacetime_create_default, which would collide at link time). */ +#define spacetime_create_default spacetime_create_default_minkowski_local +#include "../../src/spacetime_minkowski.c" +#undef spacetime_create_default +#define spacetime_create_default spacetime_create_default_schwarzschild_local +#include "../../src/spacetime_schwarzschild.c" +#undef spacetime_create_default +#define spacetime_create_default spacetime_create_default_alcubierre_local +#include "../../src/spacetime_alcubierre.c" +#undef spacetime_create_default + +#include "geodesic.h" +#include "observer.h" +#include "spacetime.h" + +#define PI 3.14159265358979323846 +#define MAX_CASES 16 +#define MAX_DIRS 16 +#define RAY_CAP 2500 + +static long g_rays = 0; + +/* ------------------------------------------------------------------ */ +/* Small helpers */ +/* ------------------------------------------------------------------ */ + +static double dot3(const double a[3], const double b[3]) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +static double ang_delta(const double a[3], const double b[3]) { + const double na = sqrt(dot3(a, a)), nb = sqrt(dot3(b, b)); + if (!(na > 0.0) || !(nb > 0.0)) + return NAN; + const double ca = dot3(a, b) / (na * nb); + const double cross[3] = {a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]}; + const double sn = sqrt(dot3(cross, cross)) / (na * nb); + return atan2(sn, ca); +} + +static double now_s(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + 1e-9 * (double)ts.tv_nsec; +} + +static int is_dark(int outcome) { return outcome == RAY_OUTCOME_DARK; } +static int is_escaped(int outcome) { return outcome == RAY_OUTCOME_ESCAPED; } + +/* Critical local angle (static observer, M=1) for the Schwarzschild monopole. + * theta_c = asin(3 sqrt(3) sqrt(1-2/r) / r). */ +static double critical_angle(double r) { + const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r; + if (!(b < 1.0)) + return PI / 2.0; + return asin(b); +} + +/* ------------------------------------------------------------------ */ +/* Case / result / comparison structures */ +/* ------------------------------------------------------------------ */ + +typedef struct { + char name[32]; + int prov; /* 0 schwarzschild, 1 minkowski, 2 alcubierre */ + double mass, escape_radius; + double alc_vs, alc_radius, alc_sigma; + double look_ra_deg, look_dec_deg; + double position[3], velocity[3]; + double initial_step, lookback; + unsigned int max_steps; + unsigned int ref_max_steps; /* budget for the tol=1e-12 reference only */ + double max_step_for_min_scan; /* fixed upper during the min scan */ + int n_dirs; + double dirs[MAX_DIRS][3]; + double theta[MAX_DIRS]; /* NAN for non-angle direction sets */ +} Case; + +typedef struct { + int outcome, reason, end_id; + double stop_t, g, thr; + double n[3]; + unsigned int steps, rejected; + unsigned long rhs; + double wall; +} Res; + +typedef struct { + int has_ref; + int class_match; + double dn_ang, dlogg, dgrel, dstopT, dmargin; +} Cmp; + +typedef struct { + long n, class_mismatch; + double max_dn_ang, max_dlogg, max_dgrel, max_dstopT, max_dmargin; + unsigned long sum_rhs, max_rhs; + unsigned int max_rejected; + double sum_wall; + long escaped, dark, unresolved, incomplete; +} Agg; + +static void agg_init(Agg *a) { + memset(a, 0, sizeof *a); + a->max_dn_ang = a->max_dlogg = a->max_dgrel = a->max_dstopT = + a->max_dmargin = -1.0; +} + +static void bump(double *m, double v) { + if (!isfinite(v)) + return; + if (v > *m) + *m = v; +} + +static void agg_add(Agg *a, const Res *r, const Cmp *c) { + ++a->n; + if (c->has_ref && !c->class_match) + ++a->class_mismatch; + if (c->has_ref) { + bump(&a->max_dn_ang, c->dn_ang); + bump(&a->max_dlogg, c->dlogg); + bump(&a->max_dgrel, c->dgrel); + bump(&a->max_dstopT, c->dstopT); + bump(&a->max_dmargin, c->dmargin); + } + a->sum_rhs += r->rhs; + if (r->rhs > a->max_rhs) + a->max_rhs = r->rhs; + if (r->rejected > a->max_rejected) + a->max_rejected = r->rejected; + a->sum_wall += r->wall; + if (is_dark(r->outcome)) + ++a->dark; + else if (is_escaped(r->outcome)) + ++a->escaped; + else if (r->outcome == RAY_OUTCOME_UNRESOLVED) + ++a->unresolved; + else + ++a->incomplete; +} + +/* ------------------------------------------------------------------ */ +/* Config builders */ +/* ------------------------------------------------------------------ */ + +static GeodesicTraceConfig make_dp(double initial, double min_step, + double max_step, double tol, + unsigned int max_steps, double lookback) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.coordinate_time_step = initial; + c.max_steps = max_steps; + c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH; + c.threshold.value = 8.0; + c.threshold.policy_version = 3; + c.stepper = GEODESIC_STEPPER_DP54; + c.atol_x = tol; + c.atol_Pi = tol; + c.atol_L = tol; + c.rtol = tol; + c.min_step = min_step; + c.max_step = max_step; + c.consecutive_rejection_limit = 32; + c.max_lookback_time = lookback; + return c; +} + +static GeodesicTraceConfig make_rk4(double step, unsigned int max_steps) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.coordinate_time_step = step; + c.max_steps = max_steps; + c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH; + c.threshold.value = 8.0; + c.threshold.policy_version = 3; + c.stepper = GEODESIC_STEPPER_RK4; + return c; +} + +/* ------------------------------------------------------------------ */ +/* Trace + compare */ +/* ------------------------------------------------------------------ */ + +static Res run_ray(const SpacetimeSource *source, const ObserverState *observer, + const double dir[3], const GeodesicTraceConfig *config) { + Res r; + memset(&r, 0, sizeof r); + ++g_rays; + if (g_rays > RAY_CAP) { + fprintf(stderr, "FATAL: ray cap %d exceeded\n", RAY_CAP); + exit(3); + } + const double t0 = now_s(); + const RayEndpoint e = geodesic_trace_past(source, observer, dir, config); + r.wall = now_s() - t0; + r.outcome = e.outcome; + r.reason = e.reason; + r.end_id = e.end_id; + r.stop_t = e.stop_coordinate_time; + r.g = e.frequency_ratio; + r.thr = e.threshold_value; + for (int i = 0; i < 3; ++i) + r.n[i] = e.n_infinity[i]; + r.steps = e.accepted_steps; + r.rejected = e.rejected_steps; + r.rhs = e.rhs_evaluations; + return r; +} + +static void compare(const Res *a, const Res *ref, Cmp *c) { + memset(c, 0, sizeof *c); + c->has_ref = 1; + c->class_match = (a->outcome == ref->outcome && a->reason == ref->reason); + if (is_escaped(a->outcome) && is_escaped(ref->outcome)) { + c->dn_ang = ang_delta(a->n, ref->n); + if (a->g > 0.0 && ref->g > 0.0 && isfinite(a->g) && isfinite(ref->g)) { + c->dlogg = fabs(log(a->g) - log(ref->g)); + c->dgrel = fabs(a->g / ref->g - 1.0); + } else { + c->dlogg = c->dgrel = NAN; + } + } else { + c->dn_ang = c->dlogg = c->dgrel = NAN; + } + c->dstopT = (isfinite(a->stop_t) && isfinite(ref->stop_t)) + ? fabs(a->stop_t - ref->stop_t) + : NAN; + c->dmargin = (is_dark(a->outcome) && is_dark(ref->outcome) && + isfinite(a->thr) && isfinite(ref->thr)) + ? fabs(a->thr - ref->thr) + : NAN; +} + +static void write_row(FILE *f, const char *phase, const Case *c, int di, + double upper, double min_step, double tol, const Res *r, + const Cmp *cmp) { + fprintf(f, + "%s,%s,%d,%.17g,%.6g,%.6g,%.6g,%d,%d,%u,%.17g,%u,%u,%lu,%.6g," + "%.17g,%.17g,%.17g,%.17g,%.17g", + phase, c->name, di, c->theta[di], upper, min_step, tol, r->outcome, + r->reason, r->end_id, r->stop_t, r->steps, r->rejected, r->rhs, + r->wall, r->n[0], r->n[1], r->n[2], r->g, r->thr); + if (cmp->has_ref) + fprintf(f, ",%d,%.17g,%.17g,%.17g,%.17g,%.17g", cmp->class_match, + cmp->dn_ang, cmp->dlogg, cmp->dgrel, cmp->dstopT, cmp->dmargin); + else + fprintf(f, ",,nan,nan,nan,nan,nan"); + fprintf(f, "\n"); +} + +static const char *ROW_HEADER = + "phase,case,dir,theta,upper,min_step,tol,outcome,reason,end_id,stop_t," + "steps,rejected,rhs,wall_s,nx,ny,nz,g,thr,class_match,dn_ang,dlogg,dgrel," + "dstopT,dmargin\n"; + +static void write_agg(FILE *f, const char *phase, const Case *c, double upper, + double min_step, double tol, const Agg *a) { + fprintf(f, + "%s,%s,%.6g,%.6g,%.6g,%ld,%ld,%.6g,%.6g,%.6g,%.6g,%.6g,%lu,%lu,%u," + "%.4f,%ld,%ld,%ld,%ld\n", + phase, c->name, upper, min_step, tol, a->n, a->class_mismatch, + a->max_dn_ang, a->max_dlogg, a->max_dgrel, a->max_dstopT, + a->max_dmargin, a->sum_rhs, a->max_rhs, a->max_rejected, a->sum_wall, + a->escaped, a->dark, a->unresolved, a->incomplete); +} + +static const char *AGG_HEADER = + "phase,case,upper,min_step,tol,n,class_mismatch,max_dn_ang,max_dlogg," + "max_dgrel,max_dstopT,max_dmargin,sum_rhs,max_rhs,max_rejected,sum_wall," + "escaped,dark,unresolved,incomplete\n"; + +/* ------------------------------------------------------------------ */ +/* Case builders */ +/* ------------------------------------------------------------------ */ + +static void dir_from_theta(double th, double n[3]) { + if (th == 0.0) { + n[0] = 1.0; + n[1] = n[2] = 0.0; + } else if (th == PI) { + n[0] = -1.0; + n[1] = n[2] = 0.0; + } else { + n[0] = cos(th); + n[1] = sin(th); + n[2] = 0.0; + } + const double nn = sqrt(dot3(n, n)); + for (int i = 0; i < 3; ++i) + n[i] /= nn; +} + +static void fill_static_dirs(Case *c, double r) { + const double tc = critical_angle(r); + const double th[MAX_DIRS] = {0.0, PI, PI / 2.0, tc, + tc - 1e-3, tc + 1e-3, tc - 1e-5, tc + 1e-5, + tc - 1e-7, tc + 1e-7, tc + 0.05, tc - 0.05}; + c->n_dirs = 12; + for (int i = 0; i < c->n_dirs; ++i) { + c->theta[i] = th[i]; + dir_from_theta(th[i], c->dirs[i]); + } +} + +static void case_static(Case *c, const char *name, double r, double look_ra) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "%s", name); + c->prov = 0; + c->mass = 1.0; + c->escape_radius = 256.0; + c->look_ra_deg = look_ra; + c->look_dec_deg = 0.0; + c->position[0] = r; + c->position[1] = c->position[2] = 0.0; + c->velocity[0] = c->velocity[1] = c->velocity[2] = 0.0; + c->initial_step = 0.1; + c->lookback = 6553.6; + c->max_steps = 65536; + c->ref_max_steps = 65536; + c->max_step_for_min_scan = 2.0; + fill_static_dirs(c, r); +} + +static void case_freefall(Case *c, const char *name, double r) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "%s", name); + c->prov = 0; + c->mass = 1.0; + c->escape_radius = 256.0; + c->look_ra_deg = 180.0; + c->look_dec_deg = 0.0; + c->position[0] = r; + c->position[1] = c->position[2] = 0.0; + /* Free-fall from rest at infinity, coordinate velocity dx/dt (the formula + * used by local/p2d_validation/observer_endpoints.c). */ + const double y = sqrt(2.0 / r); + c->velocity[0] = -y * (1.0 + y) / (1.0 + y + y * y); + c->velocity[1] = c->velocity[2] = 0.0; + c->initial_step = 0.1; + c->lookback = 6553.6; + c->max_steps = 65536; + c->ref_max_steps = 65536; + c->max_step_for_min_scan = 2.0; + { + const double th[4] = {0.0, PI / 2.0, PI, 3.0 * PI / 4.0}; + c->n_dirs = 4; + for (int i = 0; i < 4; ++i) { + c->theta[i] = th[i]; + dir_from_theta(th[i], c->dirs[i]); + } + } +} + +static void case_minkowski(Case *c) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "mink_moving"); + c->prov = 1; + c->escape_radius = 64.0; + c->look_ra_deg = 0.0; + c->look_dec_deg = 0.0; + c->position[0] = 10.0; + c->position[1] = 20.0; + c->position[2] = -15.0; + c->velocity[0] = 0.3; + c->velocity[1] = 0.2; + c->velocity[2] = 0.1; + c->initial_step = 1.0; + c->lookback = 2048.0; + c->max_steps = 2048; + c->ref_max_steps = 4096; + c->max_step_for_min_scan = 16.0; + { + const double d[6][3] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, + {0.6, 0.8, 0.0}, {-0.6, 0.8, 0.0}}; + c->n_dirs = 6; + for (int i = 0; i < 6; ++i) { + c->theta[i] = NAN; + for (int k = 0; k < 3; ++k) + c->dirs[i][k] = d[i][k]; + } + } +} + +static void case_alcubierre(Case *c, const char *name, double vs, double sigma) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "%s", name); + c->prov = 2; + c->alc_vs = vs; + c->alc_radius = 1.0; + c->alc_sigma = sigma; + c->look_ra_deg = 0.0; + c->look_dec_deg = 0.0; + c->position[0] = c->position[1] = c->position[2] = 0.0; + c->velocity[0] = vs; /* comoving with the bubble center */ + c->velocity[1] = c->velocity[2] = 0.0; + c->initial_step = fmin(0.1, 0.05 / sigma); + c->max_steps = 100000; + c->ref_max_steps = 200000; + { + const double esc = spacetime_alcubierre_escape_radius(1.0, sigma); + c->lookback = 1.25 * 4.0 * esc / (1.0 - fabs(vs)); + } + c->max_step_for_min_scan = c->initial_step * 8.0; + { + const double d[6][3] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}, + {0.6, 0.8, 0.0}, {-0.6, 0.8, 0.0}}; + c->n_dirs = 6; + for (int i = 0; i < 6; ++i) { + c->theta[i] = NAN; + for (int k = 0; k < 3; ++k) + c->dirs[i][k] = d[i][k]; + } + } +} + +/* ------------------------------------------------------------------ */ +/* Source/observer construction */ +/* ------------------------------------------------------------------ */ + +static int build_source(const Case *c, SpacetimeSource *s) { + if (c->prov == 0) + return spacetime_create_schwarzschild_ks(s, c->mass, c->escape_radius); + if (c->prov == 1) + return spacetime_create_minkowski(s, c->escape_radius); + return spacetime_create_alcubierre(s, c->alc_vs, c->alc_radius, + c->alc_sigma); +} + +static int build_observer(const Case *c, const SpacetimeSource *s, + ObserverState *o) { + MetricData m; + if (spacetime_eval(s, 0.0, c->position, &m) != SPACETIME_POINT_OK) + return -1; + ObserverCamera cam; + memset(&cam, 0, sizeof cam); + cam.coordinate_time = 0.0; + for (int i = 0; i < 3; ++i) { + cam.position[i] = c->position[i]; + cam.velocity[i] = c->velocity[i]; + } + cam.look_ra_deg = c->look_ra_deg; + cam.look_dec_deg = c->look_dec_deg; + cam.roll_deg = 0.0; + return observer_from_coordinate_camera(&m, &cam, o, NULL) == + OBSERVER_BUILD_OK + ? 0 + : -1; +} + +/* ------------------------------------------------------------------ */ +/* Generic scan driver */ +/* ------------------------------------------------------------------ */ + +static void run_grid(const Case *cases, int ncases, const double *uppers, + int n_up, const double *floors, int n_fl, double tol, + const char *phase, const char *rows_path, + const char *agg_path, Res refs[][MAX_DIRS]) { + FILE *rf = fopen(rows_path, "w"); + FILE *af = fopen(agg_path, "w"); + if (!rf || !af) { + fprintf(stderr, "FATAL: cannot open %s / %s\n", rows_path, agg_path); + exit(2); + } + fprintf(rf, "%s", ROW_HEADER); + fprintf(af, "%s", AGG_HEADER); + for (int ci = 0; ci < ncases; ++ci) { + const Case *c = &cases[ci]; + SpacetimeSource source; + ObserverState observer; + if (build_source(c, &source) || build_observer(c, &source, &observer)) { + fprintf(stderr, "FATAL: cannot build case %s\n", c->name); + exit(2); + } + for (int ui = 0; ui < n_up; ++ui) { + for (int fi = 0; fi < n_fl; ++fi) { + Agg agg; + agg_init(&agg); + for (int di = 0; di < c->n_dirs; ++di) { + const GeodesicTraceConfig cfg = + make_dp(c->initial_step, floors[fi], uppers[ui], tol, c->max_steps, + c->lookback); + const Res r = run_ray(&source, &observer, c->dirs[di], &cfg); + Cmp cmp; + compare(&r, &refs[ci][di], &cmp); + write_row(rf, phase, c, di, uppers[ui], floors[fi], tol, &r, &cmp); + agg_add(&agg, &r, &cmp); + } + write_agg(af, phase, c, uppers[ui], floors[fi], tol, &agg); + printf("SUMMARY %s %s upper=%.6g min=%.6g n=%ld mismatch=%ld " + "max_dn_ang=%.6g max_dgrel=%.6g max_dstopT=%.6g sum_rhs=%lu " + "esc=%ld dark=%ld unres=%ld inc=%ld wall=%.3fs\n", + phase, c->name, uppers[ui], floors[fi], agg.n, + agg.class_mismatch, agg.max_dn_ang, agg.max_dgrel, + agg.max_dstopT, agg.sum_rhs, agg.escaped, agg.dark, + agg.unresolved, agg.incomplete, agg.sum_wall); + fflush(stdout); + } + } + spacetime_destroy(&source); + } + fclose(rf); + fclose(af); +} + +/* Compute DP tol=1e-12 reference for every direction of every case. + * upper_ref <= 0 means "use the case's own initial step" as the max_step. */ +static void compute_refs(const Case *cases, int ncases, double upper_ref, + const char *path, Res refs[][MAX_DIRS]) { + FILE *f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "FATAL: cannot open %s\n", path); + exit(2); + } + fprintf(f, "%s", ROW_HEADER); + for (int ci = 0; ci < ncases; ++ci) { + const Case *c = &cases[ci]; + SpacetimeSource source; + ObserverState observer; + if (build_source(c, &source) || build_observer(c, &source, &observer)) { + fprintf(stderr, "FATAL: cannot build case %s\n", c->name); + exit(2); + } + const double upper = (upper_ref > 0.0) ? upper_ref : c->initial_step; + for (int di = 0; di < c->n_dirs; ++di) { + const GeodesicTraceConfig cfg = make_dp(c->initial_step, 1e-12, upper, + 1e-12, c->ref_max_steps, + c->lookback); + refs[ci][di] = run_ray(&source, &observer, c->dirs[di], &cfg); + Cmp none; + memset(&none, 0, sizeof none); + write_row(f, "ref", c, di, upper, 1e-12, 1e-12, &refs[ci][di], &none); + printf("REF %s dir=%d theta=%.6g outcome=%d reason=%d stop_t=%.9g " + "steps=%u rejected=%u rhs=%lu g=%.12g thr=%.12g\n", + c->name, di, c->theta[di], refs[ci][di].outcome, + refs[ci][di].reason, refs[ci][di].stop_t, refs[ci][di].steps, + refs[ci][di].rejected, refs[ci][di].rhs, refs[ci][di].g, + refs[ci][di].thr); + fflush(stdout); + } + spacetime_destroy(&source); + } + fclose(f); +} + +/* ------------------------------------------------------------------ */ +/* Sub-command: Schwarzschild */ +/* ------------------------------------------------------------------ */ + +static void cmd_schwarzschild(void) { + static Case cases[MAX_CASES]; + static Res refs[MAX_CASES][MAX_DIRS]; + memset(refs, 0, sizeof refs); + int nc = 0; + case_static(&cases[nc++], "r30_inward", 30.0, 180.0); + case_static(&cases[nc++], "r100_inward", 100.0, 180.0); + case_static(&cases[nc++], "r2p1_outward", 2.1, 0.0); + case_freefall(&cases[nc++], "r1p5_freefall", 1.5); + + compute_refs(cases, nc, 0.25, "raw/a_sch_reference.csv", refs); + + static const double uppers[9] = {0.25, 0.5, 1.0, 2.0, 4.0, + 8.0, 16.0, 32.0, 64.0}; + static const double floors[10] = {1e-1, 1e-2, 1e-3, 1e-4, 1e-5, + 1e-6, 1e-8, 1e-10, 1e-12, 1e-14}; + run_grid(cases, nc, uppers, 9, (double[]){1e-12}, 1, 1e-9, "upper", + "raw/a_sch_upper.csv", "raw/a_sch_upper_summary.csv", refs); + run_grid(cases, nc, (double[]){2.0}, 1, floors, 10, 1e-9, "min", + "raw/a_sch_min.csv", "raw/a_sch_min_summary.csv", refs); + + /* Sensitive-ray RK4 h-halving reference (extra independent check). + * (case index, dir index): r30 and r2.1, theta_c +- 1e-5 / +- 1e-7. */ + const int sens[8][2] = {{0, 8}, {0, 9}, {0, 6}, {0, 7}, + {2, 8}, {2, 9}, {2, 6}, {2, 7}}; + FILE *f = fopen("raw/a_sch_rk4_sensitive.csv", "w"); + if (!f) { + fprintf(stderr, "FATAL: cannot open rk4 csv\n"); + exit(2); + } + fprintf(f, "%s", ROW_HEADER); + for (int k = 0; k < 8; ++k) { + const Case *c = &cases[sens[k][0]]; + const int di = sens[k][1]; + SpacetimeSource source; + ObserverState observer; + if (build_source(c, &source) || build_observer(c, &source, &observer)) { + fprintf(stderr, "FATAL: cannot build case %s\n", c->name); + exit(2); + } + GeodesicTraceConfig rk01 = make_rk4(0.01, 262144); + GeodesicTraceConfig rk005 = make_rk4(0.005, 262144); + Res r01 = run_ray(&source, &observer, c->dirs[di], &rk01); + Res r005 = run_ray(&source, &observer, c->dirs[di], &rk005); + Cmp hh, vsdp; + compare(&r005, &r01, &hh); + compare(&r005, &refs[sens[k][0]][di], &vsdp); + Cmp none; + memset(&none, 0, sizeof none); + write_row(f, "rk4_0.01", c, di, 0.01, 0.0, 0.0, &r01, &none); + write_row(f, "rk4_0.005", c, di, 0.005, 0.0, 0.0, &r005, &none); + printf("RK4 %s dir=%d theta=%.6g o01=%d/%d o005=%d/%d " + "hhalve_dn=%.6g hhalve_dgrel=%.6g vsdp_dn=%.6g vsdp_dgrel=%.6g " + "vsdp_class=%d\n", + c->name, di, c->theta[di], r01.outcome, r01.reason, r005.outcome, + r005.reason, hh.dn_ang, hh.dgrel, vsdp.dn_ang, vsdp.dgrel, + vsdp.class_match); + spacetime_destroy(&source); + } + fclose(f); +} + +/* ------------------------------------------------------------------ */ +/* Sub-command: Minkowski (analytic flat verification) */ +/* ------------------------------------------------------------------ */ + +/* Analytic flat past escape from an observer at (t0,x0) with tetrad-frame + * direction n: coordinate photon velocity u = k_vec/k^0, crossing radius R at + * s = t-t0 < 0, sky direction n_inf = -u, frequency ratio g = 1/k^0. */ +static int mink_analytic(const Case *c, const ObserverState *o, + const double n[3], double R, double *t_cross, + double *xc, double *ninf, double *g) { + double k[4]; + for (int mu = 0; mu < 4; ++mu) + k[mu] = o->tetrad[0][mu]; + for (int a = 0; a < 3; ++a) + for (int mu = 0; mu < 4; ++mu) + k[mu] -= n[a] * o->tetrad[a + 1][mu]; + if (!(k[0] > 0.0)) + return -1; + double u[3]; + for (int i = 0; i < 3; ++i) + u[i] = k[i + 1] / k[0]; + const double uu = dot3(u, u); + const double B = 2.0 * dot3(c->position, u); + const double C = dot3(c->position, c->position) - R * R; + const double disc = B * B - 4.0 * uu * C; + if (disc < 0.0) + return -1; + const double sq = sqrt(disc); + const double s1 = (-B + sq) / (2.0 * uu); + const double s2 = (-B - sq) / (2.0 * uu); + const double s = (s1 < 0.0) ? fmin(s1, s2) : s2; + if (!(s < 0.0)) + return -1; + *t_cross = o->coordinate_time + s; + for (int i = 0; i < 3; ++i) + xc[i] = c->position[i] + u[i] * s; + const double nu = sqrt(uu); + for (int i = 0; i < 3; ++i) + ninf[i] = -u[i] / nu; + *g = 1.0 / k[0]; + return 0; +} + +static void cmd_minkowski(void) { + Case c; + case_minkowski(&c); + static Res refs[MAX_CASES][MAX_DIRS]; + memset(refs, 0, sizeof refs); + SpacetimeSource source; + ObserverState observer; + if (build_source(&c, &source) || build_observer(&c, &source, &observer)) { + fprintf(stderr, "FATAL: cannot build minkowski case\n"); + exit(2); + } + + FILE *mf = fopen("raw/a_mink_analytic.csv", "w"); + if (!mf) { + fprintf(stderr, "FATAL: cannot open analytic csv\n"); + exit(2); + } + fprintf(mf, + "case,dir,upper,min_step,tol,outcome,stop_t,x_err,t_err,dn_ang," + "dgrel,g_analytic,stop_t_analytic\n"); + + /* Reference: tol=1e-12, max_step = initial (1.0). */ + const GeodesicTraceConfig refcfg = + make_dp(c.initial_step, 1e-12, c.initial_step, 1e-12, c.max_steps, + c.lookback); + for (int di = 0; di < c.n_dirs; ++di) + refs[0][di] = run_ray(&source, &observer, c.dirs[di], &refcfg); + + static const double uppers[5] = {1.0, 4.0, 16.0, 64.0, 256.0}; + static const double floors[3] = {1e-1, 1e-6, 1e-12}; + for (int ui = 0; ui < 5; ++ui) { + for (int fi = 0; fi < 3; ++fi) { + Agg agg; + agg_init(&agg); + for (int di = 0; di < c.n_dirs; ++di) { + const GeodesicTraceConfig cfg = + make_dp(c.initial_step, floors[fi], uppers[ui], 1e-9, c.max_steps, + c.lookback); + const Res r = run_ray(&source, &observer, c.dirs[di], &cfg); + Cmp cmp; + compare(&r, &refs[0][di], &cmp); + agg_add(&agg, &r, &cmp); + double t_an, x_an[3], n_an[3], g_an; + const int ok = mink_analytic(&c, &observer, c.dirs[di], c.escape_radius, + &t_an, x_an, n_an, &g_an); + double x_err = NAN, t_err = NAN, dn_an = NAN, dg_an = NAN; + if (ok == 0 && is_escaped(r.outcome)) { + /* Crossing position is checked through the direct trace in + * a_mink_analytic_x.csv; here compare sky direction, g and time. */ + t_err = fabs(r.stop_t - t_an); + dn_an = ang_delta(r.n, n_an); + if (r.g > 0.0 && g_an > 0.0) + dg_an = fabs(r.g / g_an - 1.0); + } + fprintf(mf, + "%s,%d,%.6g,%.6g,%.6g,%d,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g," + "%.17g\n", + c.name, di, uppers[ui], floors[fi], 1e-9, r.outcome, r.stop_t, + x_err, t_err, dn_an, dg_an, g_an, t_an); + } + printf("SUMMARY mink upper=%.6g min=%.6g n=%ld mismatch=%ld " + "max_dn_ang=%.6g max_dgrel=%.6g sum_rhs=%lu esc=%ld dark=%ld " + "unres=%ld inc=%ld\n", + uppers[ui], floors[fi], agg.n, agg.class_mismatch, + agg.max_dn_ang, agg.max_dgrel, agg.sum_rhs, agg.escaped, agg.dark, + agg.unresolved, agg.incomplete); + fflush(stdout); + } + } + + /* Direct endpoint check for the crossing position (x) on a few configs. */ + FILE *xf = fopen("raw/a_mink_analytic_x.csv", "w"); + if (!xf) { + fprintf(stderr, "FATAL: cannot open analytic x csv\n"); + exit(2); + } + fprintf(xf, "case,dir,upper,min_step,tol,outcome,x_err_x,x_err_y,x_err_z," + "t_err\n"); + for (int ui = 0; ui < 5; ++ui) { + for (int fi = 0; fi < 3; ++fi) { + for (int di = 0; di < c.n_dirs; ++di) { + const GeodesicTraceConfig cfg = + make_dp(c.initial_step, floors[fi], uppers[ui], 1e-9, c.max_steps, + c.lookback); + const RayEndpoint e = + geodesic_trace_past(&source, &observer, c.dirs[di], &cfg); + ++g_rays; + double t_an, x_an[3], n_an[3], g_an; + const int ok = mink_analytic(&c, &observer, c.dirs[di], c.escape_radius, + &t_an, x_an, n_an, &g_an); + double ex = NAN, ey = NAN, ez = NAN, te = NAN; + if (ok == 0 && e.outcome == RAY_OUTCOME_ESCAPED) { + ex = fabs(e.final_x[0] - x_an[0]); + ey = fabs(e.final_x[1] - x_an[1]); + ez = fabs(e.final_x[2] - x_an[2]); + te = fabs(e.stop_coordinate_time - t_an); + } + fprintf(xf, "%s,%d,%.6g,%.6g,%.6g,%d,%.17g,%.17g,%.17g,%.17g\n", + c.name, di, uppers[ui], floors[fi], 1e-9, e.outcome, ex, ey, ez, + te); + } + } + } + fclose(xf); + fclose(mf); + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* Sub-command: Alcubierre */ +/* ------------------------------------------------------------------ */ + +static void cmd_alcubierre(void) { + static Case cases[MAX_CASES]; + static Res refs[MAX_CASES][MAX_DIRS]; + memset(refs, 0, sizeof refs); + int nc = 0; + case_alcubierre(&cases[nc++], "alc_v3_s1", 0.3, 1.0); + case_alcubierre(&cases[nc++], "alc_v3_s10", 0.3, 10.0); + case_alcubierre(&cases[nc++], "alc_v9_s1", 0.9, 1.0); + case_alcubierre(&cases[nc++], "alc_v9_s10", 0.9, 10.0); + case_alcubierre(&cases[nc++], "alc_v9_s100", 0.9, 100.0); + + compute_refs(cases, nc, -1.0, "raw/a_alc_reference.csv", refs); + + /* Standard grid: upper = initial*{1,4,8,16,32,64}, floor {1e-4,1e-8,1e-12}. */ + { + static const double floors[3] = {1e-4, 1e-8, 1e-12}; + Case sub[4]; + for (int i = 0; i < 4; ++i) + sub[i] = cases[i]; + /* run_grid uses one upper array for all cases, so use per-case initial by + * calling run_grid four times. */ + for (int i = 0; i < 4; ++i) { + char rp[128], ap[128]; + snprintf(rp, sizeof rp, "raw/a_alc_%.31s_upper.csv", sub[i].name); + snprintf(ap, sizeof ap, "raw/a_alc_%.31s_upper_summary.csv", sub[i].name); + const double base = sub[i].initial_step; + const double up[6] = {base, 4 * base, 8 * base, + 16 * base, 32 * base, 64 * base}; + run_grid(&sub[i], 1, up, 6, floors, 3, 1e-9, "upper", rp, ap, &refs[i]); + } + } + + /* Focused lower-bound stress: vs=.9, sigma=100, upper = initial*8 = 0.004, + * floors {1e-4,1e-5,1e-6,1e-8,1e-12,1e-14}. */ + { + const Case *c = &cases[4]; + const double up[1] = {c->initial_step * 8.0}; + const double floors[6] = {1e-4, 1e-5, 1e-6, 1e-8, 1e-12, 1e-14}; + run_grid(c, 1, up, 1, floors, 6, 1e-9, "min_stress", + "raw/a_alc_v9_s100_min.csv", "raw/a_alc_v9_s100_min_summary.csv", + &refs[4]); + } +} + +/* ------------------------------------------------------------------ */ + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, + "usage: %s schwarzschild|minkowski|alcubierre\n", + argv[0]); + return 1; + } + if (!strcmp(argv[1], "schwarzschild")) + cmd_schwarzschild(); + else if (!strcmp(argv[1], "minkowski")) + cmd_minkowski(); + else if (!strcmp(argv[1], "alcubierre")) + cmd_alcubierre(); + else { + fprintf(stderr, "unknown sub-command %s\n", argv[1]); + return 1; + } + printf("TOTAL_RAYS %ld\n", g_rays); + return 0; +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/b_actual_h.c b/benchmarks/adaptive_step_bounds_2026-10-05/b_actual_h.c new file mode 100644 index 0000000..c8c0ab5 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/b_actual_h.c @@ -0,0 +1,404 @@ +/* + * Experiment B: observed accepted-step sizes of the production DP54 core. + * + * This translation unit textually includes src/geodesic.c so the real, + * production `dp_advance_one` driver and the real `State` (GeodesicRayState) + * are exercised directly on a finite trajectory interval. The production + * initialization is reproduced exactly as `geodesic_trace_past` does for an + * INSIDE route: `geodesic_initialize_past_ray_metric` at the camera event, + * then `next_step = config->coordinate_time_step` (the init already sets + * `integration_start_time`, `log_alpha_p0` and `log_alpha_p0_0`). This is the + * same initialization the public endpoint uses, not a re-implementation. + * + * For each selected ray we: + * 1. run the public endpoint with tol=1e-12 to get a trusted reference + * stop time and terminal class (physical terminal class is owned by A); + * 2. integrate the same ray with the production DP core up to + * T = min(20, ref_span) observed steps <= 2000, recording each accepted + * step magnitude, boundary-limit flag, rejection/RHS deltas and the null + * residual gamma^{ij} Pi_i Pi_j - 1. + * + * Link line (geodesic.c is textually included, so it is NOT linked; the + * analytic backends are also textually included here): + * cc -std=c11 -O2 -Isrc b_actual_h.c asymptotic.c asymptotic_schwarzschild.c \ + * spacetime_common.c observer.c -lm + */ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include + +#define spacetime_create_default spacetime_create_default_minkowski_b +#include "../../src/spacetime_minkowski.c" +#undef spacetime_create_default +#define spacetime_create_default spacetime_create_default_schwarzschild_b +#include "../../src/spacetime_schwarzschild.c" +#undef spacetime_create_default +#define spacetime_create_default spacetime_create_default_alcubierre_b +#include "../../src/spacetime_alcubierre.c" +#undef spacetime_create_default + +/* The production geodesic core, textually included. Its static State, + * dp_advance_one and invert become visible to the code below. */ +#include "../../src/geodesic.c" + +#define PI 3.14159265358979323846 +#define MAX_BCASES 12 +#define MAX_BDIRS 8 +#define MAX_OBS_STEPS 2000 + +typedef struct { + char name[32]; + int prov; /* 0 sch, 1 mink, 2 alc */ + double mass, esc; + double vs, radius, sigma; + double look_ra_deg, look_dec_deg; + double pos[3], vel[3]; + double initial, max_step; + double ref_max_step, lookback; + unsigned int ref_max_steps; + int ndirs; + double dirs[MAX_BDIRS][3]; +} BCase; + +static double b_now(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + 1e-9 * (double)ts.tv_nsec; +} + +static void b_dir(double th, double n[3]) { + if (th == 0.0) { + n[0] = 1.0; + n[1] = n[2] = 0.0; + } else if (th == PI) { + n[0] = -1.0; + n[1] = n[2] = 0.0; + } else { + n[0] = cos(th); + n[1] = sin(th); + n[2] = 0.0; + } + const double nn = sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); + for (int i = 0; i < 3; ++i) + n[i] /= nn; +} + +static double b_critical_angle(double r) { + const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r; + return (b < 1.0) ? asin(b) : PI / 2.0; +} + +static GeodesicTraceConfig b_make_dp(double initial, double min_step, + double max_step, double tol, + unsigned int max_steps, double lookback) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.coordinate_time_step = initial; + c.max_steps = max_steps; + c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH; + c.threshold.value = 8.0; + c.threshold.policy_version = 3; + c.stepper = GEODESIC_STEPPER_DP54; + c.atol_x = c.atol_Pi = c.atol_L = c.rtol = tol; + c.min_step = min_step; + c.max_step = max_step; + c.consecutive_rejection_limit = 32; + c.max_lookback_time = lookback; + return c; +} + +static int b_build_source(const BCase *c, SpacetimeSource *s) { + if (c->prov == 0) + return spacetime_create_schwarzschild_ks(s, c->mass, c->esc); + if (c->prov == 1) + return spacetime_create_minkowski(s, c->esc); + return spacetime_create_alcubierre(s, c->vs, c->radius, c->sigma); +} + +static int b_build_observer(const BCase *c, const SpacetimeSource *s, + ObserverState *o) { + MetricData m; + if (spacetime_eval(s, 0.0, c->pos, &m) != SPACETIME_POINT_OK) + return -1; + ObserverCamera cam; + memset(&cam, 0, sizeof cam); + for (int i = 0; i < 3; ++i) { + cam.position[i] = c->pos[i]; + cam.velocity[i] = c->vel[i]; + } + cam.look_ra_deg = c->look_ra_deg; + cam.look_dec_deg = c->look_dec_deg; + return observer_from_coordinate_camera(&m, &cam, o, NULL) == + OBSERVER_BUILD_OK + ? 0 + : -1; +} + +static void b_add_sch(BCase *c, const char *name, double r, double look, + const double *thetas, int nt) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "%s", name); + c->prov = 0; + c->mass = 1.0; + c->esc = 256.0; + c->look_ra_deg = look; + c->pos[0] = r; + c->initial = 0.1; + c->max_step = 2.0; + c->ref_max_step = 0.25; + c->lookback = 6553.6; + c->ref_max_steps = 65536; + c->ndirs = nt; + for (int i = 0; i < nt; ++i) + b_dir(thetas[i], c->dirs[i]); +} + +static void b_add_mink(BCase *c) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "mink_moving"); + c->prov = 1; + c->esc = 64.0; + c->look_ra_deg = 0.0; + c->pos[0] = 10.0; + c->pos[1] = 20.0; + c->pos[2] = -15.0; + c->vel[0] = 0.3; + c->vel[1] = 0.2; + c->vel[2] = 0.1; + c->initial = 1.0; + c->max_step = 16.0; + c->ref_max_step = 1.0; + c->lookback = 2048.0; + c->ref_max_steps = 4096; + c->ndirs = 2; + b_dir(0.0, c->dirs[0]); + b_dir(PI, c->dirs[1]); +} + +static void b_add_alc(BCase *c, const char *name, double vs, double sigma, + const double *thetas, int nt) { + memset(c, 0, sizeof *c); + snprintf(c->name, sizeof c->name, "%s", name); + c->prov = 2; + c->vs = vs; + c->radius = 1.0; + c->sigma = sigma; + c->look_ra_deg = 0.0; + c->vel[0] = vs; + c->initial = fmin(0.1, 0.05 / sigma); + c->max_step = c->initial * 8.0; + c->ref_max_step = c->initial; + const double esc = spacetime_alcubierre_escape_radius(1.0, sigma); + c->lookback = 1.25 * 4.0 * esc / (1.0 - fabs(vs)); + c->ref_max_steps = 200000; + c->ndirs = nt; + for (int i = 0; i < nt; ++i) + b_dir(thetas[i], c->dirs[i]); +} + +/* gamma^{ij} Pi_i Pi_j - 1; `invert` is the production static from + * geodesic.c, so this is the exact production metric contraction. */ +static double b_null_residual(const MetricData *m, const double Pi[3]) { + double g[3][3], inv[3][3]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + g[i][j] = m->gamma[i][j]; + if (invert(g, inv)) + return NAN; + double v = 0.0; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + v += inv[i][j] * Pi[i] * Pi[j]; + return v - 1.0; +} + +int main(void) { + static BCase cases[MAX_BCASES]; + int nc = 0; + + { + const double tc = b_critical_angle(30.0); + const double th[6] = {0.0, PI, tc - 1e-7, tc + 1e-7, tc - 1e-5, tc + 1e-5}; + b_add_sch(&cases[nc++], "r30_inward", 30.0, 180.0, th, 6); + } + { + const double tc = b_critical_angle(100.0); + const double th[4] = {0.0, PI, tc, tc - 1e-7}; + b_add_sch(&cases[nc++], "r100_inward", 100.0, 180.0, th, 4); + } + { + const double tc = b_critical_angle(2.1); + const double th[5] = {0.0, tc, tc - 1e-7, tc + 1e-7, PI}; + b_add_sch(&cases[nc++], "r2p1_outward", 2.1, 0.0, th, 5); + } + { + const double th[2] = {0.0, PI}; + b_add_sch(&cases[nc++], "r1p5_freefall", 1.5, 180.0, th, 2); + /* free-fall velocity is set below (b_add_sch leaves it zero). */ + } + b_add_mink(&cases[nc++]); + { + const double th[2] = {0.0, PI}; + b_add_alc(&cases[nc++], "alc_v3_s1", 0.3, 1.0, th, 2); + b_add_alc(&cases[nc++], "alc_v9_s10", 0.9, 10.0, th, 2); + b_add_alc(&cases[nc++], "alc_v9_s100", 0.9, 100.0, th, 2); + } + /* Free-fall coordinate velocity for r1.5 (same formula as A). */ + { + BCase *c = &cases[3]; + const double y = sqrt(2.0 / c->pos[0]); + c->vel[0] = -y * (1.0 + y) / (1.0 + y + y * y); + } + + FILE *hf = fopen("raw/b_actual_h.csv", "w"); + FILE *sf = fopen("raw/b_summary.csv", "w"); + if (!hf || !sf) { + fprintf(stderr, "FATAL: cannot open b csv\n"); + return 2; + } + fprintf(hf, "case,dir,step,t,h,boundary_limited,rhs_delta,reject_delta," + "null_residual\n"); + fprintf(sf, "case,dir,ref_outcome,ref_stop_t,span,target,observed_steps," + "reached_target,terminated_reason,h_min,h_max,h_first,h_last," + "boundary_steps,sum_rhs,sum_reject,max_reject_delta," + "null_residual_max,final_t,wall_s\n"); + + long total_steps = 0; + for (int ci = 0; ci < nc; ++ci) { + BCase *c = &cases[ci]; + SpacetimeSource source; + ObserverState observer; + if (b_build_source(c, &source) || b_build_observer(c, &source, &observer)) { + fprintf(stderr, "FATAL: cannot build B case %s\n", c->name); + return 2; + } + for (int di = 0; di < c->ndirs; ++di) { + /* 1. trusted reference via the public endpoint (physical terminal). */ + GeodesicTraceConfig refcfg = + b_make_dp(c->initial, 1e-12, c->ref_max_step, 1e-12, c->ref_max_steps, + c->lookback); + RayEndpoint ref = + geodesic_trace_past(&source, &observer, c->dirs[di], &refcfg); + const double span = + isfinite(ref.stop_coordinate_time) + ? (observer.coordinate_time - ref.stop_coordinate_time) + : 20.0; + const double finite_T = fmin(20.0, span > 0.0 ? span : 20.0); + const double target = observer.coordinate_time - finite_T; + + /* 2. production-exact INSIDE-route initialization. */ + MetricData metric; + if (spacetime_eval(&source, observer.coordinate_time, + observer.coordinate_position, &metric) != + SPACETIME_POINT_OK) { + fprintf(stderr, "FATAL: camera metric %s\n", c->name); + return 2; + } + State st; + if (geodesic_initialize_past_ray_metric(&metric, &observer, c->dirs[di], + &st)) { + fprintf(stderr, "FATAL: init %s dir %d\n", c->name, di); + return 2; + } + GeodesicTraceConfig cfg = + b_make_dp(c->initial, 1e-12, c->max_step, 1e-9, 0u, 0.0); + st.next_step = cfg.coordinate_time_step; + + MetricSlab *slab = NULL; + if (spacetime_load_slab(&source, st.coordinate_time, target - 1.0, + &slab)) { + fprintf(stderr, "FATAL: slab %s dir %d\n", c->name, di); + return 2; + } + + double h_min = INFINITY, h_max = 0.0, h_first = NAN, h_last = NAN; + double null_max = 0.0, sum_wall = 0.0; + unsigned long sum_rhs = 0; + unsigned int sum_rej = 0, max_rej_delta = 0, boundary_steps = 0; + int reached = 0, terminated_reason = -1; + int step_index = 0; + const double start_wall = b_now(); + while (st.coordinate_time > target && step_index < MAX_OBS_STEPS) { + const double before_t = st.coordinate_time; + unsigned long rhs = st.rhs_evaluations; + unsigned int rej = st.rejected_steps; + RayReason reason = RAY_REASON_INTEGRATION_ERROR; + const int rc = + dp_advance_one(slab, &cfg, &st, target, &reason, &rhs, &rej, NULL); + const unsigned long rhs_delta = rhs - st.rhs_evaluations; + const unsigned int rej_delta = rej - st.rejected_steps; + st.rhs_evaluations = rhs; + st.rejected_steps = rej; + sum_rhs += rhs_delta; + sum_rej += rej_delta; + if (rej_delta > max_rej_delta) + max_rej_delta = rej_delta; + if (rc) { + terminated_reason = (int)reason; + break; + } + const double h = before_t - st.coordinate_time; + const int boundary = (st.coordinate_time == target); + if (boundary) + ++boundary_steps; + if (h < h_min) + h_min = h; + if (h > h_max) + h_max = h; + if (step_index == 0) + h_first = h; + h_last = h; + ++step_index; + MetricData m; + double nr = NAN; + if (spacetime_slab_eval(slab, st.coordinate_time, st.x, &m) == + SPACETIME_POINT_OK) + nr = b_null_residual(&m, st.Pi); + if (isfinite(nr) && fabs(nr) > null_max) + null_max = fabs(nr); + fprintf(hf, "%s,%d,%d,%.17g,%.17g,%d,%lu,%u,%.6g\n", c->name, di, + step_index, st.coordinate_time, h, boundary, rhs_delta, + rej_delta, nr); + if (st.coordinate_time <= target) + reached = 1; + } + sum_wall = b_now() - start_wall; + if (step_index >= MAX_OBS_STEPS) + reached = 0; + if (!isfinite(h_first)) { + h_first = NAN; + h_max = NAN; + } + if (!isfinite(h_min) || h_min == INFINITY) + h_min = NAN; + if (h_last < 0.0) + h_last = NAN; + fprintf(sf, + "%s,%d,%d,%.17g,%.17g,%.17g,%d,%d,%d,%.17g,%.17g,%.17g,%.17g," + "%u,%lu,%u,%u,%.6g,%.17g,%.6g\n", + c->name, di, ref.outcome, ref.stop_coordinate_time, span, target, + step_index, reached, terminated_reason, h_min, h_max, h_first, + h_last, boundary_steps, sum_rhs, sum_rej, max_rej_delta, null_max, + st.coordinate_time, sum_wall); + printf("B %s dir=%d ref=%d refstop=%.9g span=%.6g T=%.6g target=%.6g " + "steps=%d reached=%d term=%d h=[%.6g,%.6g] first=%.6g last=%.6g " + "boundary=%u rhs=%lu rej=%u nullmax=%.3g final_t=%.9g wall=%.4fs\n", + c->name, di, ref.outcome, ref.stop_coordinate_time, span, finite_T, + target, step_index, reached, terminated_reason, h_min, h_max, + h_first, h_last, boundary_steps, sum_rhs, sum_rej, null_max, + st.coordinate_time, sum_wall); + fflush(stdout); + total_steps += step_index; + spacetime_free_slab(slab); + } + spacetime_destroy(&source); + } + fclose(hf); + fclose(sf); + printf("TOTAL_OBSERVED_STEPS %ld\n", total_steps); + return 0; +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c b/benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c new file mode 100644 index 0000000..5a4ee19 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c @@ -0,0 +1,341 @@ +/* + * Standalone critical-classification reference check. + * + * Three static Schwarzschild cameras (M=1, escape sphere 256): + * r30 look 180 (inward), r100 look 180 (inward), r2.1 look 0 (outward), + * each aimed at theta_crit exactly and at theta_crit +- 1e-7, where + * theta_crit = asin(3 sqrt(3) sqrt(1 - 2/r) / r) + * is the local tetrad angle from the camera forward direction. + * + * For every direction four DP54 configs are run (initial step 0.1, + * min_step 1e-14, max_steps 65536, max_lookback_time 6553.6, reject limit 32, + * camera-relative dark threshold 8): + * + * ref_tol1e-12_h0.25 tol 1e-12, max_step 0.25 + * ref_tol1e-13_h0.125 tol 1e-13, max_step 0.125 + * tol1e-11_h2 tol 1e-11, max_step 2 + * tol1e-11_h8 tol 1e-11, max_step 8 + * + * Total 3 cameras * 3 directions * 4 configs = 36 endpoint traces. + * + * Raw per-run rows go to raw/critical_ref.csv; stdout prints, for all nine + * directions, ref_vs_tighter / h2_vs_h8 / ref_vs_h2 / ref_vs_h8 comparisons + * (outcome/reason/end, sky angle, relative g, dark stop-time and threshold + * shifts) and a NEIGHBOR_REFERENCE_CHECK that verifies the two reference + * tiers agree at the +-1e-7 neighbours. The exactly-critical direction is a + * separatrix: a class flip or a large direction/stop-time difference there is + * reported as EXACT_CRITICAL_REFERENCE_NOT_CONVERGED and is diagnostic only. + * + * Exit status: 0 when the neighbours' reference tiers agree and no neighbour + * returned an error/unresolved endpoint or a zero-step trace; 1 when a + * neighbour reference check fails; 2 on setup error. The exact-critical + * separatrix is never an assertion. + * + * Link line (the Schwarzschild backend is textually included): + * cc -std=c11 -O2 -Isrc critical_ref_check.c geodesic.c asymptotic.c \ + * asymptotic_schwarzschild.c spacetime_common.c observer.c -lm + */ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include + +#include "../../src/spacetime_schwarzschild.c" + +#include "geodesic.h" +#include "observer.h" +#include "spacetime.h" + +#define PI 3.14159265358979323846 + +typedef struct { + const char *name; + double tol; + double max_step; +} CfgDef; + +static const CfgDef cfgs[4] = { + {"ref_tol1e-12_h0.25", 1e-12, 0.25}, + {"ref_tol1e-13_h0.125", 1e-13, 0.125}, + {"tol1e-11_h2", 1e-11, 2.0}, + {"tol1e-11_h8", 1e-11, 8.0}, +}; + +typedef struct { + const char *name; + double r; + double look_ra_deg; +} CamDef; + +static const CamDef cams[3] = { + {"r30_inward", 30.0, 180.0}, + {"r100_inward", 100.0, 180.0}, + {"r2p1_outward", 2.1, 0.0}, +}; + +typedef struct { + int outcome, reason; + unsigned int end_id; + double stop_t, g, thr; + double L, L0; + double n[3]; + unsigned int steps, rejected; + unsigned long rhs; +} Res; + +static double b_dot3(const double a[3], const double b[3]) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +static double b_ang_delta(const double a[3], const double b[3]) { + const double na = sqrt(b_dot3(a, a)), nb = sqrt(b_dot3(b, b)); + if (!(na > 0.0) || !(nb > 0.0)) + return NAN; + const double cross[3] = {a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]}; + return atan2(sqrt(b_dot3(cross, cross)) / (na * nb), + b_dot3(a, b) / (na * nb)); +} + +static double critical_angle(double r) { + const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r; + return (b < 1.0) ? asin(b) : PI / 2.0; +} + +static void dir_from_theta(double th, double n[3]) { + n[0] = cos(th); + n[1] = sin(th); + n[2] = 0.0; + const double nn = sqrt(b_dot3(n, n)); + for (int i = 0; i < 3; ++i) + n[i] /= nn; +} + +static GeodesicTraceConfig make_cfg(const CfgDef *d) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.coordinate_time_step = 0.1; + c.max_steps = 65536; + c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH; + c.threshold.value = 8.0; + c.threshold.policy_version = 3; + c.stepper = GEODESIC_STEPPER_DP54; + c.atol_x = c.atol_Pi = c.atol_L = c.rtol = d->tol; + c.min_step = 1e-14; + c.max_step = d->max_step; + c.consecutive_rejection_limit = 32; + c.max_lookback_time = 6553.6; + return c; +} + +static int build_observer(const CamDef *cam, const SpacetimeSource *s, + ObserverState *o) { + const double pos[3] = {cam->r, 0.0, 0.0}; + MetricData m; + if (spacetime_eval(s, 0.0, pos, &m) != SPACETIME_POINT_OK) + return -1; + ObserverCamera camera; + memset(&camera, 0, sizeof camera); + camera.position[0] = pos[0]; + camera.position[1] = pos[1]; + camera.position[2] = pos[2]; + camera.look_ra_deg = cam->look_ra_deg; + camera.look_dec_deg = 0.0; + return observer_from_coordinate_camera(&m, &camera, o, NULL) == + OBSERVER_BUILD_OK + ? 0 + : -1; +} + +static Res run_one(const SpacetimeSource *s, const ObserverState *o, + const double dir[3], const GeodesicTraceConfig *cfg) { + Res r; + memset(&r, 0, sizeof r); + const RayEndpoint e = geodesic_trace_past(s, o, dir, cfg); + r.outcome = e.outcome; + r.reason = e.reason; + r.end_id = e.end_id; + r.stop_t = e.stop_coordinate_time; + r.g = e.frequency_ratio; + r.thr = e.threshold_value; + r.L = e.final_log_alpha_p0; + r.L0 = e.final_log_alpha_p0_0; + for (int i = 0; i < 3; ++i) + r.n[i] = e.n_infinity[i]; + r.steps = e.accepted_steps; + r.rejected = e.rejected_steps; + r.rhs = e.rhs_evaluations; + return r; +} + +static const char *outcome_name(int o) { + switch (o) { + case RAY_OUTCOME_ESCAPED: + return "ESC"; + case RAY_OUTCOME_DARK: + return "DARK"; + case RAY_OUTCOME_UNRESOLVED: + return "UNRES"; + default: + return "INC"; + } +} + +static void print_pair(const char *cam, int di, double theta, const char *pn, + const Res *a, const Res *b) { + const int same = a->outcome == b->outcome && a->reason == b->reason && + a->end_id == b->end_id; + const int esc = a->outcome == RAY_OUTCOME_ESCAPED && + b->outcome == RAY_OUTCOME_ESCAPED; + const double dn = esc ? b_ang_delta(a->n, b->n) : NAN; + const double dg = + (esc && a->g > 0.0 && b->g > 0.0) ? fabs(a->g / b->g - 1.0) : NAN; + const double dstop = + (isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t + : NAN; + const double dthr = (isfinite(a->thr) && isfinite(b->thr)) + ? a->thr - b->thr + : NAN; + printf("CAPAIR camera=%s dir=%d theta=%.17g pair=%s class=%s/%s same=%d " + "dn_ang=%.17g dgrel=%.17g dstop_t=%.17g dthr=%.17g end=%u/%u\n", + cam, di, theta, pn, outcome_name(a->outcome), outcome_name(b->outcome), + same, dn, dg, dstop, dthr, a->end_id, b->end_id); +} + +/* A two-reference-tier agreement check for one neighbour direction. Returns + * 1 when the pair is a valid, agreeing ESC/DARK reference, 0 otherwise. */ +static int neighbour_reference_check(const char *cam, int di, double theta, + const Res *a, const Res *b) { + const int class_same = a->outcome == b->outcome && a->reason == b->reason && + a->end_id == b->end_id; + const int clean = (a->outcome == RAY_OUTCOME_ESCAPED || + a->outcome == RAY_OUTCOME_DARK) && + (b->outcome == RAY_OUTCOME_ESCAPED || + b->outcome == RAY_OUTCOME_DARK); + const int stepped = a->steps > 0 && b->steps > 0; + const int esc = a->outcome == RAY_OUTCOME_ESCAPED && + b->outcome == RAY_OUTCOME_ESCAPED; + const double dn = esc ? b_ang_delta(a->n, b->n) : NAN; + const double dg = + (esc && a->g > 0.0 && b->g > 0.0) ? fabs(a->g / b->g - 1.0) : NAN; + const double dstop = + (isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t + : NAN; + printf("NEIGHBOR_REFERENCE_CHECK camera=%s dir=%d theta=%.17g " + "ref_class0=%s/%d/%u ref_class1=%s/%d/%u same=%d clean=%d stepped=%d " + "dn_ang=%.17g dgrel=%.17g dstop_t=%.17g\n", + cam, di, theta, outcome_name(a->outcome), a->reason, a->end_id, + outcome_name(b->outcome), b->reason, b->end_id, class_same, clean, + stepped, dn, dg, dstop); + return class_same && clean && stepped; +} + +int main(void) { + SpacetimeSource source; + if (spacetime_create_schwarzschild_ks(&source, 1.0, 256.0)) { + fprintf(stderr, "FATAL: cannot create Schwarzschild source\n"); + return 2; + } + + FILE *f = fopen("raw/critical_ref.csv", "w"); + if (!f) { + fprintf(stderr, "FATAL: cannot open raw/critical_ref.csv\n"); + return 2; + } + fprintf(f, "camera,dir,theta,cfg,outcome,reason,end_id,stop_t,steps," + "rejected,rhs,nx,ny,nz,g,thr,L,L0\n"); + + Res res[3][3][4]; + double thetas[3][3]; + for (int ci = 0; ci < 3; ++ci) { + ObserverState observer; + if (build_observer(&cams[ci], &source, &observer)) { + fprintf(stderr, "FATAL: cannot build observer %s\n", cams[ci].name); + return 2; + } + const double tc = critical_angle(cams[ci].r); + const double th[3] = {tc - 1e-7, tc, tc + 1e-7}; + for (int di = 0; di < 3; ++di) { + double dir[3]; + dir_from_theta(th[di], dir); + thetas[ci][di] = th[di]; + for (int fi = 0; fi < 4; ++fi) { + const GeodesicTraceConfig cfg = make_cfg(&cfgs[fi]); + const Res r = run_one(&source, &observer, dir, &cfg); + res[ci][di][fi] = r; + fprintf(f, "%s,%d,%.17g,%s,%d,%d,%u,%.17g,%u,%u,%lu,%.17g,%.17g," + "%.17g,%.17g,%.17g,%.17g,%.17g\n", + cams[ci].name, di, th[di], cfgs[fi].name, r.outcome, r.reason, + r.end_id, r.stop_t, r.steps, r.rejected, r.rhs, r.n[0], r.n[1], + r.n[2], r.g, r.thr, r.L, r.L0); + printf("RAW camera=%s dir=%d theta=%.17g cfg=%s outcome=%d reason=%d " + "end=%u stop_t=%.17g steps=%u rejected=%u rhs=%lu g=%.17g " + "thr=%.17g L=%.17g L0=%.17g nx=%.17g ny=%.17g nz=%.17g\n", + cams[ci].name, di, th[di], cfgs[fi].name, r.outcome, r.reason, + r.end_id, r.stop_t, r.steps, r.rejected, r.rhs, r.g, r.thr, r.L, + r.L0, r.n[0], r.n[1], r.n[2]); + fflush(stdout); + } + } + } + fclose(f); + + /* All nine directions: ref_vs_tighter, h2_vs_h8, ref_vs_h2, ref_vs_h8. */ + for (int ci = 0; ci < 3; ++ci) { + for (int di = 0; di < 3; ++di) { + print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_tighter", + &res[ci][di][0], &res[ci][di][1]); + print_pair(cams[ci].name, di, thetas[ci][di], "h2_vs_h8", + &res[ci][di][2], &res[ci][di][3]); + print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_h2", + &res[ci][di][0], &res[ci][di][2]); + print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_h8", + &res[ci][di][0], &res[ci][di][3]); + } + } + + /* Exact-critical: diagnostic only, never asserted. */ + for (int ci = 0; ci < 3; ++ci) { + const Res *a = &res[ci][1][0]; + const Res *b = &res[ci][1][1]; + const int same = a->outcome == b->outcome && a->reason == b->reason && + a->end_id == b->end_id; + const int esc = a->outcome == RAY_OUTCOME_ESCAPED && + b->outcome == RAY_OUTCOME_ESCAPED; + const int dark = a->outcome == RAY_OUTCOME_DARK && + b->outcome == RAY_OUTCOME_DARK; + const double dn = esc ? b_ang_delta(a->n, b->n) : NAN; + const double dstop = + (isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t + : NAN; + const int nonconverged = + !same || (esc && !(dn <= 1e-6)) || (dark && fabs(dstop) > 1e-4); + printf("EXACT_CRITICAL_DIRECTION camera=%s theta=%.17g ref0=%s/%d/%u " + "ref1=%s/%d/%u same=%d dn_ang=%.17g dstop_t=%.17g\n", + cams[ci].name, thetas[ci][1], outcome_name(a->outcome), a->reason, + a->end_id, outcome_name(b->outcome), b->reason, b->end_id, same, dn, + dstop); + if (nonconverged) + printf("EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=%s dn_ang=%.17g " + "dstop_t=%.17g class_same=%d\n", + cams[ci].name, dn, dstop, same); + } + + /* Neighbour reference check: must pass; this is the assertion-valid path. */ + int fail = 0; + for (int ci = 0; ci < 3; ++ci) { + for (int di = 0; di < 3; di += 2) { + if (!neighbour_reference_check(cams[ci].name, di, thetas[ci][di], + &res[ci][di][0], &res[ci][di][1])) + fail = 1; + } + } + + spacetime_destroy(&source); + printf("CRITICAL_REFERENCE_CHECK status=%d\n", fail ? 1 : 0); + printf("CRITICAL_REF_DONE\n"); + return fail ? 1 : 0; +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/failure_floor.c b/benchmarks/adaptive_step_bounds_2026-10-05/failure_floor.c new file mode 100644 index 0000000..4d5714f --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/failure_floor.c @@ -0,0 +1,26 @@ +/* Diagnostic only: reuse the production regression's invalid-metric fixture. */ +#define main adaptive_regression_main_not_called +#include "../../tests/test_geodesic_adaptive.c" +#undef main + +int main(void) { + const double floors[] = {1e-2,1e-4,1e-6,1e-8,1e-10,1e-12,1e-14}; + for (unsigned i=0;i/local/adaptive_bounds_mesh. Writes metadata.json in +OUT_DIR. No renderer or test is run. +""" +import hashlib +import json +from pathlib import Path +import subprocess +import sys +import time + +ROOT = Path(__file__).resolve().parents[2] +OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \ + ROOT / 'local/adaptive_bounds_mesh' +PATHS = ['src/geodesic.c', 'src/main.c', 'src/frame.c', + 'build/Release/schwarzschild_sky'] + +metadata = { + 'recorded_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), + 'git_head': subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(), + 'hashes': {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() + for p in PATHS}, + 'compiler': subprocess.check_output(['cc', '--version'], text=True), + 'cpu': subprocess.check_output(['lscpu'], text=True), + 'scope': '8 bounded 320x180 candidates plus 2 tight references; no 4K here', + 'threads': 8, + 'build_command': 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend', + 'run_commands': [ + 'python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py [OUT]', + 'python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py [OUT]', + 'bash benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh [OUT]'], +} +OUT.mkdir(parents=True, exist_ok=True) +(OUT / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n') +print(json.dumps(metadata, indent=2)) diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_attempts.json b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_attempts.json new file mode 100644 index 0000000..c220ab3 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_attempts.json @@ -0,0 +1,16 @@ +[ + { + "upper": 2, + "command": "/home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 2 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png", + "state": "completed", + "returncode": 0, + "wall_seconds": 11.389317202148959 + }, + { + "upper": 8, + "command": "/home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 8 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png", + "state": "completed", + "returncode": 0, + "wall_seconds": 8.389249739935622 + } +] diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_final_summary.json b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_final_summary.json new file mode 100644 index 0000000..745e577 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_final_summary.json @@ -0,0 +1,160 @@ +{ + "r100_4k_hmax2": { + "vertices": 66045, + "triangles": 131338, + "outcomes": { + "0": 62419, + "1": 3626 + }, + "reasons": { + "0": 62419, + "1": 3626 + }, + "accepted": 15768677, + "rejected": 97176, + "rhs": 130409139, + "provenance": { + "atol_x": 1e-09, + "atol_Pi": 1e-09, + "atol_L": 1e-09, + "rtol": 1e-09, + "min_step": 1e-12, + "max_step": 2.0, + "max_lookback_time": 6553.6, + "retry_lookback_increment": 6553.6, + "max_total_lookback_time": 26214.4, + "integrator": 1, + "initial_step": 0.1, + "initial_max_steps": 65536, + "threshold": 8.0, + "retry_step_increment": 65536, + "max_total_steps": 262144 + }, + "accepted_percentiles": { + "0": 162, + "0.5": 212, + "0.9": 328, + "0.99": 404, + "1": 520 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 1, + "0.9": 3, + "0.99": 4, + "1": 5 + }, + "rhs_percentiles": { + "0": 1421, + "0.5": 1799, + "0.9": 2597, + "0.99": 3129, + "1": 3941 + }, + "sha256": "ef7267074338192f8597b1f2a0300fd36da8e25441e064719c7ad586c481ae7a", + "triangle_sha256": "2a943902e9b1401a16bc40fc547c19077bb12e396080074393636ee30761b7b2", + "unique_film_positions": 66045, + "duplicate_film_positions": 0, + "requested_samples_including_nonpersistent_probes": 116308, + "initial_samples": 32776, + "refinement_samples_per_generation": [ + 32686, + 7900, + 13375, + 24826, + 4403, + 261, + 67, + 14 + ], + "trace_wall_seconds": 10.774, + "wall_seconds": 11.389317202148959 + }, + "r100_4k_hmax8": { + "vertices": 66045, + "triangles": 131338, + "outcomes": { + "0": 62419, + "1": 3626 + }, + "reasons": { + "0": 62419, + "1": 3626 + }, + "accepted": 10183020, + "rejected": 194781, + "rhs": 91991144, + "provenance": { + "atol_x": 1e-09, + "atol_Pi": 1e-09, + "atol_L": 1e-09, + "rtol": 1e-09, + "min_step": 1e-12, + "max_step": 8.0, + "max_lookback_time": 6553.6, + "retry_lookback_increment": 6553.6, + "max_total_lookback_time": 26214.4, + "integrator": 1, + "initial_step": 0.1, + "initial_max_steps": 65536, + "threshold": 8.0, + "retry_step_increment": 65536, + "max_total_steps": 262144 + }, + "accepted_percentiles": { + "0": 80, + "0.5": 127, + "0.9": 259, + "0.99": 337, + "1": 432 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 3, + "0.9": 5, + "0.99": 6, + "1": 7 + }, + "rhs_percentiles": { + "0": 868, + "0.5": 1204, + "0.9": 2114, + "0.99": 2646, + "1": 3318 + }, + "sha256": "b1897493b7fa5b88f86872e81d15ac876818a705fa0fefe8b1ee262da9de59bb", + "triangle_sha256": "2a943902e9b1401a16bc40fc547c19077bb12e396080074393636ee30761b7b2", + "unique_film_positions": 66045, + "duplicate_film_positions": 0, + "requested_samples_including_nonpersistent_probes": 116308, + "initial_samples": 32776, + "refinement_samples_per_generation": [ + 32686, + 7900, + 13375, + 24826, + 4403, + 261, + 67, + 14 + ], + "trace_wall_seconds": 7.805000000000001, + "wall_seconds": 8.389249739935622 + }, + "comparison": { + "shared": 66045, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 7.954413592847793e-07, + "max_logg_difference": 3.471110204822381e-11, + "triangle_payload_identical": true + }, + "relative_reduction": { + "accepted": 0.35422483446138187, + "rhs": 0.2945958795111745, + "wall_seconds": 0.2634106513116764, + "trace_wall_seconds": 0.27557081863746047 + }, + "cost_scope": "accepted/rejected/RHS sums cover final persistent vertices only; nonpersistent discarded refinement probes are not in v3 map. Do not interpret them as full-render executed RHS totals." +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax2.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax2.log new file mode 100644 index 0000000..cbe376d --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax2.log @@ -0,0 +1,242 @@ +OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE /home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 2 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png +Blackbody LUT: 1024 CIE 1931 2-deg 1 nm-linear XYZ nodes, T=[670.146556, 101408.88] K, loaded assets/blackbody/cie1931_2deg_xyz_1024.grbblut in 0.000040 s; payload fnv1a64=0e39867b0e70a809 +Blackbody backend: lut +PSF cache ready: 64x64 phases, radius 25 px, relative tail 1e-04, tail abs 1e-06, boundary 1e-07, build 0.250 s +Frame 0: integrator=dp54 step=0.1 max_steps=65536 threshold=8 atol=(x 1e-09, Pi 1e-09, L 1e-09) rtol=1e-09 bounds=[1e-12, 2] max_rejections=32 lookback=6553.6 +Frame 0: tracing 32776 initial rays from 64800 mesh triangles... +Frame 0: initial ray trace finished in 2.562 s. +Frame 0: starting adaptive ray-trace refinement (max level 4)... +Frame 0: refinement generation 0 tracing 32686 samples from 32776 vertices and 64800 triangles. +Frame 0: refinement generation 0 finished; added 3014 vertices, now 35790 vertices and 70828 triangles. +Frame 0: refinement generation 1 tracing 7900 samples from 35790 vertices and 70828 triangles. +Frame 0: refinement generation 1 finished; added 4852 vertices, now 40642 vertices and 80532 triangles. +Frame 0: refinement generation 2 tracing 13375 samples from 40642 vertices and 80532 triangles. +Frame 0: refinement generation 2 finished; added 8889 vertices, now 49531 vertices and 98310 triangles. +Frame 0: refinement generation 3 tracing 24826 samples from 49531 vertices and 98310 triangles. +Frame 0: refinement generation 3 finished; added 16055 vertices, now 65586 vertices and 130420 triangles. +Frame 0: refinement generation 4 tracing 4403 samples from 65586 vertices and 130420 triangles. +Frame 0: refinement generation 4 finished; added 389 vertices, now 65975 vertices and 131198 triangles. +Frame 0: refinement generation 5 tracing 261 samples from 65975 vertices and 131198 triangles. +Frame 0: refinement generation 5 finished; added 58 vertices, now 66033 vertices and 131314 triangles. +Frame 0: refinement generation 6 tracing 67 samples from 66033 vertices and 131314 triangles. +Frame 0: refinement generation 6 finished; added 12 vertices, now 66045 vertices and 131338 triangles. +Frame 0: refinement generation 7 tracing 14 samples from 66045 vertices and 131338 triangles. +Frame 0: refinement generation 7 finished; added 0 vertices, now 66045 vertices and 131338 triangles. +Frame 0: adaptive ray-trace refinement finished in 8.212 s; 66045 vertices, 131338 triangles. +Boundary accounting: EEE=122456 DDD=5624 EED/EDD=3258 UUD/UDD=0 U+E=0 UUU=0 error=0; approx-black=0 (0 px^2), retries=0, budget-incomplete=0. +Frame 0 trace cost: accepted=15768677 rejected=97176 rhs=130409139 over 66045 vertices (saturated=0). +Wrote lens map: /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens (66045 vertices, 131338 triangles) +Frame 0: traced 66045 lens vertices; starting catalog render. +Frame 0: finding and prefetching catalog tiles... +Frame 0: catalog prefetch finished (0 candidate tiles). +Frame 0: splatting 131338 lens triangles... +Frame 0: splat worker 1/16 started. +Frame 0: splat worker 1/16 reached 8 local triangles. +Frame 0: splat worker 1/16 reached 16 local triangles. +Frame 0: splat worker 1/16 reached 32 local triangles. +Frame 0: splat worker 6/16 started. +Frame 0: splat worker 8/16 started. +Frame 0: splat worker 8/16 reached 8 local triangles. +Frame 0: splat worker 8/16 reached 16 local triangles. +Frame 0: splat worker 12/16 started. +Frame 0: splat worker 13/16 started. +Frame 0: splat worker 3/16 started. +Frame 0: splat worker 6/16 reached 8 local triangles. +Frame 0: splat worker 6/16 reached 16 local triangles. +Frame 0: splat worker 9/16 started. +Frame 0: splat worker 11/16 started. +Frame 0: splat worker 7/16 started. +Frame 0: splat worker 7/16 reached 8 local triangles. +Frame 0: splat worker 13/16 reached 8 local triangles. +Frame 0: splat worker 13/16 reached 16 local triangles. +Frame 0: splat worker 13/16 reached 32 local triangles. +Frame 0: splat worker 3/16 reached 8 local triangles. +Frame 0: splat worker 3/16 reached 16 local triangles. +Frame 0: splat worker 3/16 reached 32 local triangles. +Frame 0: splat worker 15/16 started. +Frame 0: splat worker 15/16 reached 8 local triangles. +Frame 0: splat worker 15/16 reached 16 local triangles. +Frame 0: splat worker 15/16 reached 32 local triangles. +Frame 0: splat worker 10/16 started. +Frame 0: splat worker 8/16 reached 32 local triangles. +Frame 0: splat worker 9/16 reached 8 local triangles. +Frame 0: splat worker 9/16 reached 16 local triangles. +Frame 0: splat worker 12/16 reached 8 local triangles. +Frame 0: splat worker 7/16 reached 16 local triangles. +Frame 0: splat worker 14/16 started. +Frame 0: splat worker 4/16 started. +Frame 0: splat worker 13/16 reached 64 local triangles. +Frame 0: splat worker 2/16 started. +Frame 0: splat worker 5/16 started. +Frame 0: splat worker 5/16 reached 8 local triangles. +Frame 0: splat worker 5/16 reached 16 local triangles. +Frame 0: splat worker 1/16 reached 64 local triangles. +Frame 0: splat worker 16/16 started. +Frame 0: splat worker 16/16 reached 8 local triangles. +Frame 0: splat worker 16/16 reached 16 local triangles. +Frame 0: splat worker 8/16 reached 64 local triangles. +Frame 0: splat worker 11/16 reached 8 local triangles. +Frame 0: splat worker 8/16 reached 128 local triangles. +Frame 0: splat worker 12/16 reached 16 local triangles. +Frame 0: splat worker 7/16 reached 32 local triangles. +Frame 0: splat worker 14/16 reached 8 local triangles. +Frame 0: splat worker 8/16 reached 256 local triangles. +Frame 0: splat worker 13/16 reached 128 local triangles. +Frame 0: splat worker 2/16 reached 8 local triangles. +Frame 0: splat worker 2/16 reached 16 local triangles. +Frame 0: splat worker 6/16 reached 32 local triangles. +Frame 0: splat worker 5/16 reached 32 local triangles. +Frame 0: splat worker 6/16 reached 64 local triangles. +Frame 0: splat worker 1/16 reached 128 local triangles. +Frame 0: splat worker 5/16 reached 64 local triangles. +Frame 0: splat worker 10/16 reached 8 local triangles. +Frame 0: splat worker 9/16 reached 32 local triangles. +Frame 0: splat worker 6/16 reached 128 local triangles. +Frame 0: splat worker 11/16 reached 16 local triangles. +Frame 0: splat worker 5/16 reached 128 local triangles. +Frame 0: splat worker 8/16 reached 512 local triangles. +Frame 0: splat worker 7/16 reached 64 local triangles. +Frame 0: splat worker 14/16 reached 16 local triangles. +Frame 0: splat worker 4/16 reached 8 local triangles. +Frame 0: splat worker 2/16 reached 32 local triangles. +Frame 0: splat worker 15/16 reached 64 local triangles. +Frame 0: splat worker 7/16 reached 128 local triangles. +Frame 0: splat worker 13/16 reached 256 local triangles. +Frame 0: splat worker 10/16 reached 16 local triangles. +Frame 0: splat worker 5/16 reached 256 local triangles. +Frame 0: splat worker 11/16 reached 32 local triangles. +Frame 0: splat worker 9/16 reached 64 local triangles. +Frame 0: splat worker 1/16 reached 256 local triangles. +Frame 0: splat worker 11/16 reached 64 local triangles. +Frame 0: splat worker 3/16 reached 64 local triangles. +Frame 0: splat worker 14/16 reached 32 local triangles. +Frame 0: splat worker 4/16 reached 16 local triangles. +Frame 0: splat worker 6/16 reached 256 local triangles. +Frame 0: splat worker 4/16 reached 32 local triangles. +Frame 0: splat worker 14/16 reached 64 local triangles. +Frame 0: splat worker 11/16 reached 128 local triangles. +Frame 0: splat worker 3/16 reached 128 local triangles. +Frame 0: splat worker 10/16 reached 32 local triangles. +Frame 0: splat worker 4/16 reached 64 local triangles. +Frame 0: splat worker 7/16 reached 256 local triangles. +Frame 0: splat worker 10/16 reached 64 local triangles. +Frame 0: splat worker 16/16 reached 32 local triangles. +Frame 0: splat worker 4/16 reached 128 local triangles. +Frame 0: splat worker 12/16 reached 32 local triangles. +Frame 0: splat worker 5/16 reached 512 local triangles. +Frame 0: splat worker 11/16 reached 256 local triangles. +Frame 0: splat worker 16/16 reached 64 local triangles. +Frame 0: splat worker 3/16 reached 256 local triangles. +Frame 0: splat worker 12/16 reached 64 local triangles. +Frame 0: splat worker 13/16 reached 512 local triangles. +Frame 0: splat worker 15/16 reached 128 local triangles. +Frame 0: splat worker 14/16 reached 128 local triangles. +Frame 0: splat worker 9/16 reached 128 local triangles. +Frame 0: splat worker 10/16 reached 128 local triangles. +Frame 0: splat worker 2/16 reached 64 local triangles. +Frame 0: splat worker 12/16 reached 128 local triangles. +Frame 0: splat worker 16/16 reached 128 local triangles. +Frame 0: splat worker 6/16 reached 512 local triangles. +Frame 0: splat worker 4/16 reached 256 local triangles. +Frame 0: splat worker 1/16 reached 512 local triangles. +Frame 0: splat worker 8/16 reached 1024 local triangles. +Frame 0: splat worker 2/16 reached 128 local triangles. +Frame 0: splat worker 15/16 reached 256 local triangles. +Frame 0: splat worker 14/16 reached 256 local triangles. +Frame 0: splat worker 7/16 reached 512 local triangles. +Frame 0: splat worker 10/16 reached 256 local triangles. +Frame 0: splat worker 9/16 reached 256 local triangles. +Frame 0: splat worker 11/16 reached 512 local triangles. +Frame 0: splat worker 16/16 reached 256 local triangles. +Frame 0: splat worker 12/16 reached 256 local triangles. +Frame 0: splat worker 2/16 reached 256 local triangles. +Frame 0: splat worker 3/16 reached 512 local triangles. +Frame 0: splat worker 4/16 reached 512 local triangles. +Frame 0: splat worker 15/16 reached 512 local triangles. +Frame 0: splat worker 14/16 reached 512 local triangles. +Frame 0: splat worker 10/16 reached 512 local triangles. +Frame 0: splat worker 9/16 reached 512 local triangles. +Frame 0: splat worker 5/16 reached 1024 local triangles. +Frame 0: splat worker 12/16 reached 512 local triangles. +Frame 0: splat worker 16/16 reached 512 local triangles. +Frame 0: splat worker 13/16 reached 1024 local triangles. +Frame 0: splat worker 2/16 reached 512 local triangles. +Frame 0: splat worker 1/16 reached 1024 local triangles. +Frame 0: splat worker 6/16 reached 1024 local triangles. +Frame 0: splat worker 7/16 reached 1024 local triangles. +Frame 0: splat worker 11/16 reached 1024 local triangles. +Frame 0: splat worker 4/16 reached 1024 local triangles. +Frame 0: splat worker 3/16 reached 1024 local triangles. +Frame 0: splat worker 15/16 reached 1024 local triangles. +Frame 0: splat worker 14/16 reached 1024 local triangles. +Frame 0: splat worker 10/16 reached 1024 local triangles. +Frame 0: splat worker 9/16 reached 1024 local triangles. +Frame 0: splat worker 12/16 reached 1024 local triangles. +Frame 0: splat worker 16/16 reached 1024 local triangles. +Frame 0: splat worker 2/16 reached 1024 local triangles. +Frame 0: splat worker 8/16 reached 2048 local triangles. +Frame 0: splat worker 5/16 reached 2048 local triangles. +Frame 0: splat worker 13/16 reached 2048 local triangles. +Frame 0: splat worker 1/16 reached 2048 local triangles. +Frame 0: splat worker 6/16 reached 2048 local triangles. +Frame 0: splat worker 7/16 reached 2048 local triangles. +Frame 0: splat worker 11/16 reached 2048 local triangles. +Frame 0: splat worker 4/16 reached 2048 local triangles. +Frame 0: splat worker 15/16 reached 2048 local triangles. +Frame 0: splat worker 3/16 reached 2048 local triangles. +Frame 0: splat worker 14/16 reached 2048 local triangles. +Frame 0: splat worker 9/16 reached 2048 local triangles. +Frame 0: splat worker 10/16 reached 2048 local triangles. +Frame 0: splat worker 12/16 reached 2048 local triangles. +Frame 0: splat worker 2/16 reached 2048 local triangles. +Frame 0: splat worker 16/16 reached 2048 local triangles. +Frame 0: splat worker 8/16 reached 4096 local triangles. +Frame 0: splat worker 5/16 reached 4096 local triangles. +Frame 0: splat worker 13/16 reached 4096 local triangles. +Frame 0: splat worker 1/16 reached 4096 local triangles. +Frame 0: splat worker 6/16 reached 4096 local triangles. +Frame 0: splat worker 11/16 reached 4096 local triangles. +Frame 0: splat worker 4/16 reached 4096 local triangles. +Frame 0: splat worker 14/16 reached 4096 local triangles. +Frame 0: splat worker 15/16 reached 4096 local triangles. +Frame 0: splat worker 9/16 reached 4096 local triangles. +Frame 0: splat worker 10/16 reached 4096 local triangles. +Frame 0: splat worker 16/16 reached 4096 local triangles. +Frame 0: splat worker 2/16 reached 4096 local triangles. +Frame 0: splat worker 12/16 reached 4096 local triangles. +Frame 0: splat worker 3/16 reached 4096 local triangles. +Frame 0: splat worker 8/16 reached 8192 local triangles. +Frame 0: splat worker 13/16 reached 8192 local triangles. +Frame 0: splat worker 5/16 reached 8192 local triangles. +Frame 0: splat worker 12/16 reached 8192 local triangles. +Frame 0: splat worker 1/16 reached 8192 local triangles. +Frame 0: splat worker 6/16 reached 8192 local triangles. +Frame 0: splat worker 11/16 reached 8192 local triangles. +Frame 0: splat worker 14/16 reached 8192 local triangles. +Frame 0: splat worker 4/16 reached 8192 local triangles. +Frame 0: splat worker 9/16 reached 8192 local triangles. +Frame 0: splat worker 16/16 reached 8192 local triangles. +Frame 0: splat worker 10/16 reached 8192 local triangles. +Frame 0: splat worker 2/16 reached 8192 local triangles. +Frame 0: splat worker 7/16 reached 4096 local triangles. +Frame 0: splat worker 15/16 reached 8192 local triangles. +Frame 0: splat worker 6/16 finished after 8555 local triangles. +Frame 0: splat worker 16/16 finished after 8230 local triangles. +Frame 0: splat worker 15/16 finished after 8192 local triangles. +Frame 0: splat worker 11/16 finished after 8468 local triangles. +Frame 0: splat worker 10/16 finished after 8207 local triangles. +Frame 0: splat worker 5/16 finished after 8824 local triangles. +Frame 0: splat worker 4/16 finished after 8382 local triangles. +Frame 0: splat worker 3/16 finished after 8004 local triangles. +Frame 0: splat worker 8/16 finished after 9239 local triangles. +Frame 0: splat worker 1/16 finished after 8561 local triangles. +Frame 0: splat worker 12/16 finished after 8804 local triangles. +Frame 0: splat worker 14/16 finished after 8429 local triangles. +Frame 0: splat worker 7/16 finished after 4096 local triangles. +Frame 0: splat worker 2/16 finished after 8201 local triangles. +Frame 0: splat worker 9/16 finished after 8290 local triangles. +Frame 0: splat worker 13/16 finished after 8856 local triangles. +Frame 0: catalog splatting finished in 0.1 s; writing image... +Rendered 1965 images from 1 catalog stars to /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png (ok) +PSF splats: cached 0, cached wing-clipped 0, direct fallbacks 0, discarded below min-Y 1965 +Warning: --psf-min-y discarded one or more PSF events. diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax8.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax8.log new file mode 100644 index 0000000..878b970 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_hmax8.log @@ -0,0 +1,233 @@ +OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE /home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 8 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png +Blackbody LUT: 1024 CIE 1931 2-deg 1 nm-linear XYZ nodes, T=[670.146556, 101408.88] K, loaded assets/blackbody/cie1931_2deg_xyz_1024.grbblut in 0.000041 s; payload fnv1a64=0e39867b0e70a809 +Blackbody backend: lut +PSF cache ready: 64x64 phases, radius 25 px, relative tail 1e-04, tail abs 1e-06, boundary 1e-07, build 0.223 s +Frame 0: integrator=dp54 step=0.1 max_steps=65536 threshold=8 atol=(x 1e-09, Pi 1e-09, L 1e-09) rtol=1e-09 bounds=[1e-12, 8] max_rejections=32 lookback=6553.6 +Frame 0: tracing 32776 initial rays from 64800 mesh triangles... +Frame 0: initial ray trace finished in 1.735 s. +Frame 0: starting adaptive ray-trace refinement (max level 4)... +Frame 0: refinement generation 0 tracing 32686 samples from 32776 vertices and 64800 triangles. +Frame 0: refinement generation 0 finished; added 3014 vertices, now 35790 vertices and 70828 triangles. +Frame 0: refinement generation 1 tracing 7900 samples from 35790 vertices and 70828 triangles. +Frame 0: refinement generation 1 finished; added 4852 vertices, now 40642 vertices and 80532 triangles. +Frame 0: refinement generation 2 tracing 13375 samples from 40642 vertices and 80532 triangles. +Frame 0: refinement generation 2 finished; added 8889 vertices, now 49531 vertices and 98310 triangles. +Frame 0: refinement generation 3 tracing 24826 samples from 49531 vertices and 98310 triangles. +Frame 0: refinement generation 3 finished; added 16055 vertices, now 65586 vertices and 130420 triangles. +Frame 0: refinement generation 4 tracing 4403 samples from 65586 vertices and 130420 triangles. +Frame 0: refinement generation 4 finished; added 389 vertices, now 65975 vertices and 131198 triangles. +Frame 0: refinement generation 5 tracing 261 samples from 65975 vertices and 131198 triangles. +Frame 0: refinement generation 5 finished; added 58 vertices, now 66033 vertices and 131314 triangles. +Frame 0: refinement generation 6 tracing 67 samples from 66033 vertices and 131314 triangles. +Frame 0: refinement generation 6 finished; added 12 vertices, now 66045 vertices and 131338 triangles. +Frame 0: refinement generation 7 tracing 14 samples from 66045 vertices and 131338 triangles. +Frame 0: refinement generation 7 finished; added 0 vertices, now 66045 vertices and 131338 triangles. +Frame 0: adaptive ray-trace refinement finished in 6.070 s; 66045 vertices, 131338 triangles. +Boundary accounting: EEE=122456 DDD=5624 EED/EDD=3258 UUD/UDD=0 U+E=0 UUU=0 error=0; approx-black=0 (0 px^2), retries=0, budget-incomplete=0. +Frame 0 trace cost: accepted=10183020 rejected=194781 rhs=91991144 over 66045 vertices (saturated=0). +Wrote lens map: /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens (66045 vertices, 131338 triangles) +Frame 0: traced 66045 lens vertices; starting catalog render. +Frame 0: finding and prefetching catalog tiles... +Frame 0: catalog prefetch finished (0 candidate tiles). +Frame 0: splatting 131338 lens triangles... +Frame 0: splat worker 1/16 started. +Frame 0: splat worker 1/16 reached 8 local triangles. +Frame 0: splat worker 1/16 reached 16 local triangles. +Frame 0: splat worker 10/16 started. +Frame 0: splat worker 10/16 reached 8 local triangles. +Frame 0: splat worker 2/16 started. +Frame 0: splat worker 2/16 reached 8 local triangles. +Frame 0: splat worker 11/16 started. +Frame 0: splat worker 12/16 started. +Frame 0: splat worker 12/16 reached 8 local triangles. +Frame 0: splat worker 12/16 reached 16 local triangles. +Frame 0: splat worker 7/16 started. +Frame 0: splat worker 16/16 started. +Frame 0: splat worker 16/16 reached 8 local triangles. +Frame 0: splat worker 16/16 reached 16 local triangles. +Frame 0: splat worker 16/16 reached 32 local triangles. +Frame 0: splat worker 16/16 reached 64 local triangles. +Frame 0: splat worker 16/16 reached 128 local triangles. +Frame 0: splat worker 16/16 reached 256 local triangles. +Frame 0: splat worker 15/16 started. +Frame 0: splat worker 8/16 started. +Frame 0: splat worker 16/16 reached 512 local triangles. +Frame 0: splat worker 8/16 reached 8 local triangles. +Frame 0: splat worker 11/16 reached 8 local triangles. +Frame 0: splat worker 10/16 reached 16 local triangles. +Frame 0: splat worker 14/16 started. +Frame 0: splat worker 5/16 started. +Frame 0: splat worker 6/16 started. +Frame 0: splat worker 6/16 reached 8 local triangles. +Frame 0: splat worker 9/16 started. +Frame 0: splat worker 7/16 reached 8 local triangles. +Frame 0: splat worker 7/16 reached 16 local triangles. +Frame 0: splat worker 16/16 reached 1024 local triangles. +Frame 0: splat worker 4/16 started. +Frame 0: splat worker 3/16 started. +Frame 0: splat worker 8/16 reached 16 local triangles. +Frame 0: splat worker 3/16 reached 8 local triangles. +Frame 0: splat worker 10/16 reached 32 local triangles. +Frame 0: splat worker 16/16 reached 2048 local triangles. +Frame 0: splat worker 5/16 reached 8 local triangles. +Frame 0: splat worker 10/16 reached 64 local triangles. +Frame 0: splat worker 5/16 reached 16 local triangles. +Frame 0: splat worker 5/16 reached 32 local triangles. +Frame 0: splat worker 10/16 reached 128 local triangles. +Frame 0: splat worker 10/16 reached 256 local triangles. +Frame 0: splat worker 10/16 reached 512 local triangles. +Frame 0: splat worker 1/16 reached 32 local triangles. +Frame 0: splat worker 6/16 reached 16 local triangles. +Frame 0: splat worker 12/16 reached 32 local triangles. +Frame 0: splat worker 1/16 reached 64 local triangles. +Frame 0: splat worker 9/16 reached 8 local triangles. +Frame 0: splat worker 2/16 reached 16 local triangles. +Frame 0: splat worker 2/16 reached 32 local triangles. +Frame 0: splat worker 2/16 reached 64 local triangles. +Frame 0: splat worker 2/16 reached 128 local triangles. +Frame 0: splat worker 10/16 reached 1024 local triangles. +Frame 0: splat worker 2/16 reached 256 local triangles. +Frame 0: splat worker 4/16 reached 8 local triangles. +Frame 0: splat worker 11/16 reached 16 local triangles. +Frame 0: splat worker 4/16 reached 16 local triangles. +Frame 0: splat worker 11/16 reached 32 local triangles. +Frame 0: splat worker 4/16 reached 32 local triangles. +Frame 0: splat worker 14/16 reached 8 local triangles. +Frame 0: splat worker 5/16 reached 64 local triangles. +Frame 0: splat worker 14/16 reached 16 local triangles. +Frame 0: splat worker 7/16 reached 32 local triangles. +Frame 0: splat worker 12/16 reached 64 local triangles. +Frame 0: splat worker 5/16 reached 128 local triangles. +Frame 0: splat worker 7/16 reached 64 local triangles. +Frame 0: splat worker 12/16 reached 128 local triangles. +Frame 0: splat worker 7/16 reached 128 local triangles. +Frame 0: splat worker 5/16 reached 256 local triangles. +Frame 0: splat worker 13/16 started. +Frame 0: splat worker 8/16 reached 32 local triangles. +Frame 0: splat worker 3/16 reached 16 local triangles. +Frame 0: splat worker 2/16 reached 512 local triangles. +Frame 0: splat worker 8/16 reached 64 local triangles. +Frame 0: splat worker 5/16 reached 512 local triangles. +Frame 0: splat worker 8/16 reached 128 local triangles. +Frame 0: splat worker 4/16 reached 64 local triangles. +Frame 0: splat worker 10/16 reached 2048 local triangles. +Frame 0: splat worker 16/16 reached 4096 local triangles. +Frame 0: splat worker 14/16 reached 32 local triangles. +Frame 0: splat worker 9/16 reached 16 local triangles. +Frame 0: splat worker 4/16 reached 128 local triangles. +Frame 0: splat worker 14/16 reached 64 local triangles. +Frame 0: splat worker 7/16 reached 256 local triangles. +Frame 0: splat worker 12/16 reached 256 local triangles. +Frame 0: splat worker 3/16 reached 32 local triangles. +Frame 0: splat worker 11/16 reached 64 local triangles. +Frame 0: splat worker 3/16 reached 64 local triangles. +Frame 0: splat worker 8/16 reached 256 local triangles. +Frame 0: splat worker 6/16 reached 32 local triangles. +Frame 0: splat worker 4/16 reached 256 local triangles. +Frame 0: splat worker 9/16 reached 32 local triangles. +Frame 0: splat worker 11/16 reached 128 local triangles. +Frame 0: splat worker 3/16 reached 128 local triangles. +Frame 0: splat worker 15/16 reached 8 local triangles. +Frame 0: splat worker 5/16 reached 1024 local triangles. +Frame 0: splat worker 15/16 reached 16 local triangles. +Frame 0: splat worker 14/16 reached 128 local triangles. +Frame 0: splat worker 9/16 reached 64 local triangles. +Frame 0: splat worker 1/16 reached 128 local triangles. +Frame 0: splat worker 7/16 reached 512 local triangles. +Frame 0: splat worker 6/16 reached 64 local triangles. +Frame 0: splat worker 15/16 reached 32 local triangles. +Frame 0: splat worker 11/16 reached 256 local triangles. +Frame 0: splat worker 3/16 reached 256 local triangles. +Frame 0: splat worker 13/16 reached 8 local triangles. +Frame 0: splat worker 15/16 reached 64 local triangles. +Frame 0: splat worker 13/16 reached 16 local triangles. +Frame 0: splat worker 9/16 reached 128 local triangles. +Frame 0: splat worker 14/16 reached 256 local triangles. +Frame 0: splat worker 2/16 reached 1024 local triangles. +Frame 0: splat worker 13/16 reached 32 local triangles. +Frame 0: splat worker 15/16 reached 128 local triangles. +Frame 0: splat worker 6/16 reached 128 local triangles. +Frame 0: splat worker 12/16 reached 512 local triangles. +Frame 0: splat worker 13/16 reached 64 local triangles. +Frame 0: splat worker 4/16 reached 512 local triangles. +Frame 0: splat worker 8/16 reached 512 local triangles. +Frame 0: splat worker 1/16 reached 256 local triangles. +Frame 0: splat worker 9/16 reached 256 local triangles. +Frame 0: splat worker 13/16 reached 128 local triangles. +Frame 0: splat worker 15/16 reached 256 local triangles. +Frame 0: splat worker 6/16 reached 256 local triangles. +Frame 0: splat worker 3/16 reached 512 local triangles. +Frame 0: splat worker 11/16 reached 512 local triangles. +Frame 0: splat worker 14/16 reached 512 local triangles. +Frame 0: splat worker 13/16 reached 256 local triangles. +Frame 0: splat worker 1/16 reached 512 local triangles. +Frame 0: splat worker 9/16 reached 512 local triangles. +Frame 0: splat worker 7/16 reached 1024 local triangles. +Frame 0: splat worker 15/16 reached 512 local triangles. +Frame 0: splat worker 6/16 reached 512 local triangles. +Frame 0: splat worker 13/16 reached 512 local triangles. +Frame 0: splat worker 4/16 reached 1024 local triangles. +Frame 0: splat worker 8/16 reached 1024 local triangles. +Frame 0: splat worker 3/16 reached 1024 local triangles. +Frame 0: splat worker 11/16 reached 1024 local triangles. +Frame 0: splat worker 14/16 reached 1024 local triangles. +Frame 0: splat worker 12/16 reached 1024 local triangles. +Frame 0: splat worker 1/16 reached 1024 local triangles. +Frame 0: splat worker 5/16 reached 2048 local triangles. +Frame 0: splat worker 15/16 reached 1024 local triangles. +Frame 0: splat worker 9/16 reached 1024 local triangles. +Frame 0: splat worker 6/16 reached 1024 local triangles. +Frame 0: splat worker 13/16 reached 1024 local triangles. +Frame 0: splat worker 7/16 reached 2048 local triangles. +Frame 0: splat worker 2/16 reached 2048 local triangles. +Frame 0: splat worker 4/16 reached 2048 local triangles. +Frame 0: splat worker 8/16 reached 2048 local triangles. +Frame 0: splat worker 10/16 reached 4096 local triangles. +Frame 0: splat worker 3/16 reached 2048 local triangles. +Frame 0: splat worker 14/16 reached 2048 local triangles. +Frame 0: splat worker 11/16 reached 2048 local triangles. +Frame 0: splat worker 1/16 reached 2048 local triangles. +Frame 0: splat worker 15/16 reached 2048 local triangles. +Frame 0: splat worker 9/16 reached 2048 local triangles. +Frame 0: splat worker 6/16 reached 2048 local triangles. +Frame 0: splat worker 13/16 reached 2048 local triangles. +Frame 0: splat worker 12/16 reached 2048 local triangles. +Frame 0: splat worker 5/16 reached 4096 local triangles. +Frame 0: splat worker 7/16 reached 4096 local triangles. +Frame 0: splat worker 4/16 reached 4096 local triangles. +Frame 0: splat worker 8/16 reached 4096 local triangles. +Frame 0: splat worker 14/16 reached 4096 local triangles. +Frame 0: splat worker 16/16 reached 8192 local triangles. +Frame 0: splat worker 3/16 reached 4096 local triangles. +Frame 0: splat worker 11/16 reached 4096 local triangles. +Frame 0: splat worker 1/16 reached 4096 local triangles. +Frame 0: splat worker 15/16 reached 4096 local triangles. +Frame 0: splat worker 9/16 reached 4096 local triangles. +Frame 0: splat worker 6/16 reached 4096 local triangles. +Frame 0: splat worker 13/16 reached 4096 local triangles. +Frame 0: splat worker 2/16 reached 4096 local triangles. +Frame 0: splat worker 12/16 reached 4096 local triangles. +Frame 0: splat worker 10/16 reached 8192 local triangles. +Frame 0: splat worker 5/16 reached 8192 local triangles. +Frame 0: splat worker 14/16 reached 8192 local triangles. +Frame 0: splat worker 7/16 reached 8192 local triangles. +Frame 0: splat worker 10/16 finished after 10255 local triangles. +Frame 0: splat worker 13/16 finished after 7729 local triangles. +Frame 0: splat worker 14/16 finished after 8310 local triangles. +Frame 0: splat worker 3/16 finished after 8102 local triangles. +Frame 0: splat worker 12/16 finished after 5601 local triangles. +Frame 0: splat worker 2/16 finished after 6132 local triangles. +Frame 0: splat worker 7/16 finished after 8294 local triangles. +Frame 0: splat worker 6/16 finished after 7791 local triangles. +Frame 0: splat worker 5/16 finished after 8901 local triangles. +Frame 0: splat worker 4/16 finished after 8159 local triangles. +Frame 0: splat worker 1/16 finished after 7930 local triangles. +Frame 0: splat worker 16/16 finished after 12209 local triangles. +Frame 0: splat worker 15/16 finished after 7871 local triangles. +Frame 0: splat worker 9/16 finished after 7811 local triangles. +Frame 0: splat worker 11/16 finished after 8075 local triangles. +Frame 0: splat worker 8/16 finished after 8168 local triangles. +Frame 0: catalog splatting finished in 0.1 s; writing image... +Rendered 1954 images from 1 catalog stars to /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png (ok) +PSF splats: cached 0, cached wing-clipped 0, direct fallbacks 0, discarded below min-Y 1954 +Warning: --psf-min-y discarded one or more PSF events. diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_metadata.json b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_metadata.json new file mode 100644 index 0000000..5428485 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_metadata.json @@ -0,0 +1,13 @@ +{ + "binary_sha256": "72ba409cd60bc72a7f0ca16f736cab6be6d4ab3f42a11d00647eeb03cc7a7e2a", + "utc": "2026-10-05T22:19:23Z", + "source_sha256": { + "src/main.c": "6cf3f6354b52b5eb029e1f3605c403c4161bb3df5d4202a47b97409318358ff9", + "src/geodesic.c": "25f24a2e1c9ce49508fed230dfb3f6d9986e92bb4a5e6347bb2356ac0ee5fffa", + "src/frame.c": "317cf2a777be139a2cab6a1b9f00094b7834fbb0382389960526ee3281ca55bf" + }, + "catalog": "one negligible synthetic point; no survey catalog", + "scope": "README R100 45-degree 3840x2160 coarse16 refine4 jacobian0.2", + "threads": 16, + "max_attempts": 2 +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_raw_summary.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_raw_summary.log new file mode 100644 index 0000000..4cedb36 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/4k_raw_summary.log @@ -0,0 +1,3 @@ +r100_4k_hmax2 {"vertices": 66045, "triangles": 131338, "outcomes": {"0": 62419, "1": 3626}, "reasons": {"0": 62419, "1": 3626}, "accepted": 15768677, "rejected": 97176, "rhs": 130409139, "provenance": {"atol_x": 1e-09, "atol_Pi": 1e-09, "atol_L": 1e-09, "rtol": 1e-09, "min_step": 1e-12, "max_step": 2.0, "max_lookback_time": 6553.6, "retry_lookback_increment": 6553.6, "max_total_lookback_time": 26214.4, "integrator": 1, "initial_step": 0.1, "initial_max_steps": 65536, "threshold": 8.0, "retry_step_increment": 65536, "max_total_steps": 262144}, "accepted_percentiles": {"0": 162, "0.5": 212, "0.9": 328, "0.99": 404, "1": 520}, "rejected_percentiles": {"0": 0, "0.5": 1, "0.9": 3, "0.99": 4, "1": 5}, "rhs_percentiles": {"0": 1421, "0.5": 1799, "0.9": 2597, "0.99": 3129, "1": 3941}, "sha256": "ef7267074338192f8597b1f2a0300fd36da8e25441e064719c7ad586c481ae7a", "unique_film_positions": 66045, "duplicate_film_positions": 0, "wall_seconds": 11.389317202148959} +r100_4k_hmax8 {"vertices": 66045, "triangles": 131338, "outcomes": {"0": 62419, "1": 3626}, "reasons": {"0": 62419, "1": 3626}, "accepted": 10183020, "rejected": 194781, "rhs": 91991144, "provenance": {"atol_x": 1e-09, "atol_Pi": 1e-09, "atol_L": 1e-09, "rtol": 1e-09, "min_step": 1e-12, "max_step": 8.0, "max_lookback_time": 6553.6, "retry_lookback_increment": 6553.6, "max_total_lookback_time": 26214.4, "integrator": 1, "initial_step": 0.1, "initial_max_steps": 65536, "threshold": 8.0, "retry_step_increment": 65536, "max_total_steps": 262144}, "accepted_percentiles": {"0": 80, "0.5": 127, "0.9": 259, "0.99": 337, "1": 432}, "rejected_percentiles": {"0": 0, "0.5": 3, "0.9": 5, "0.99": 6, "1": 7}, "rhs_percentiles": {"0": 868, "0.5": 1204, "0.9": 2114, "0.99": 2646, "1": 3318}, "sha256": "b1897493b7fa5b88f86872e81d15ac876818a705fa0fefe8b1ee262da9de59bb", "unique_film_positions": 66045, "duplicate_film_positions": 0, "wall_seconds": 8.389249739935622} +comparison {"shared": 66045, "only_a": 0, "only_b": 0, "terminal_mismatches": 0, "max_sky_angle": 7.954413592847793e-07, "max_logg_difference": 3.471110204822381e-11} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.csv b/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.csv new file mode 100644 index 0000000..11eaff7 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.csv @@ -0,0 +1,37 @@ +camera,dir,theta,cfg,outcome,reason,end_id,stop_t,steps,rejected,rhs,nx,ny,nz,g,thr,L,L0 +r30_inward,0,0.16812279429621627,ref_tol1e-12_h0.25,1,1,4294967295,-142.17574796859728,2081,0,14854,0,0,0,0,8.0000000000000195,8.0658840965825682,0.065884096582549498 +r30_inward,0,0.16812279429621627,ref_tol1e-13_h0.125,1,1,4294967295,-142.17574845681202,3332,0,23611,0,0,0,0,8.0000000000000249,8.0658840965825735,0.065884096582549498 +r30_inward,0,0.16812279429621627,tol1e-11_h2,1,1,4294967295,-142.17574172483768,1304,0,9415,0,0,0,0,8.0000000000000266,8.0658840965825753,0.065884096582549498 +r30_inward,0,0.16812279429621627,tol1e-11_h8,1,1,4294967295,-142.17574172483768,1304,0,9415,0,0,0,0,8.0000000000000266,8.0658840965825753,0.065884096582549498 +r30_inward,1,0.16812289429621627,ref_tol1e-12_h0.25,1,1,4294967295,-225.85293500418322,3746,0,26509,0,0,0,0,8.0000000000000338,8.0658840955358357,0.065884095535802364 +r30_inward,1,0.16812289429621627,ref_tol1e-13_h0.125,1,1,4294967295,-238.90906331559862,6388,0,45003,0,0,0,0,8.000000000000016,8.065884095535818,0.065884095535802364 +r30_inward,1,0.16812289429621627,tol1e-11_h2,1,1,4294967295,-212.59582609170599,2180,0,15547,0,0,0,0,8.0000000000000071,8.0658840955358091,0.065884095535802364 +r30_inward,1,0.16812289429621627,tol1e-11_h8,1,1,4294967295,-212.59582609170599,2180,0,15547,0,0,0,0,8.0000000000000071,8.0658840955358091,0.065884095535802364 +r30_inward,2,0.16812299429621627,ref_tol1e-12_h0.25,0,0,0,-370.18047830118235,2790,1,19831,-0.35873499147611138,-6.5068267720987458e-16,-0.9334394495041628,1.0350983390130453,nan,-0.038385896473527227,0.065884094489054593 +r30_inward,2,0.16812299429621627,ref_tol1e-13_h0.125,0,0,0,-370.1804778092,4857,1,34300,-0.35873507984449715,-6.5068265353608199e-16,-0.93343941554284182,1.035098339013504,nan,-0.038385896473972475,0.065884094489054593 +r30_inward,2,0.16812299429621627,tol1e-11_h2,0,0,0,-370.18048452857704,1299,1,9387,-0.3587338729380895,-6.5068297686427377e-16,-0.93343987937458439,1.0350983390054869,nan,-0.038385896466199748,0.065884094489054593 +r30_inward,2,0.16812299429621627,tol1e-11_h8,0,0,0,-370.1804845285946,1257,1,9100,-0.35873387293809145,-6.5068297686427416e-16,-0.93343987937458361,1.035098339005424,nan,-0.038385896466138665,0.065884094489054593 +r100_inward,0,0.051461896376274734,ref_tol1e-12_h0.25,1,1,4294967295,-211.027405235164,2252,0,16051,0,0,0,0,8.0000000000000266,8.0199767087115159,0.01997670871149013 +r100_inward,0,0.051461896376274734,ref_tol1e-13_h0.125,1,1,4294967295,-211.02740538583069,3727,0,26376,0,0,0,0,8.0000000000000142,8.0199767087115035,0.01997670871149013 +r100_inward,0,0.051461896376274734,tol1e-11_h2,1,1,4294967295,-211.02740354768991,1292,0,9331,0,0,0,0,8.0000000000000533,8.0199767087115426,0.01997670871149013 +r100_inward,0,0.051461896376274734,tol1e-11_h8,1,1,4294967295,-211.02740354768991,1292,0,9331,0,0,0,0,8.0000000000000533,8.0199767087115426,0.01997670871149013 +r100_inward,1,0.051461996376274736,ref_tol1e-12_h0.25,1,1,4294967295,-301.07740508539132,4045,0,28602,0,0,0,0,8.0000000000000018,8.0199767086106277,0.019976708610626077 +r100_inward,1,0.051461996376274736,ref_tol1e-13_h0.125,1,1,4294967295,-313.77734009633099,6986,0,49189,0,0,0,0,8.0000000000000018,8.0199767086106277,0.019976708610626077 +r100_inward,1,0.051461996376274736,tol1e-11_h2,1,1,4294967295,-288.21364790845217,2256,0,16079,0,0,0,0,8.0000000000000639,8.0199767086106899,0.019976708610626077 +r100_inward,1,0.051461996376274736,tol1e-11_h8,1,1,4294967295,-288.21364790845217,2256,0,16079,0,0,0,0,8.0000000000000639,8.0199767086106899,0.019976708610626077 +r100_inward,2,0.051462096376274739,ref_tol1e-12_h0.25,0,0,0,-439.31355204311825,2969,0,21077,-0.99110324423183982,-3.1326762563025509e-16,-0.13309530146899301,1.0101525445517785,nan,-0.013990814384499865,0.019976708509762027 +r100_inward,2,0.051462096376274739,ref_tol1e-13_h0.125,0,0,0,-439.31355190087947,5265,0,37149,-0.99110324787406912,-3.132675617927086e-16,-0.13309527434688151,1.010152544552184,nan,-0.013990814384902493,0.019976708509762027 +r100_inward,2,0.051462096376274739,tol1e-11_h2,0,0,0,-439.31355372933643,1291,1,9338,-0.99110320105936889,-3.1326838231562615e-16,-0.13309562295534913,1.010152544545182,nan,-0.013990814377948021,0.019976708509762027 +r100_inward,2,0.051462096376274739,tol1e-11_h8,0,0,0,-439.31355372935258,1250,1,9051,-0.991103201059369,-3.1326838231562231e-16,-0.13309562295534744,1.0101525445451216,nan,-0.013990814377887956,0.019976708509762027 +r2p1_outward,0,0.57037671003940071,ref_tol1e-12_h0.25,0,0,0,-333.45070432558816,2639,1,18774,-0.30976840652656795,0,-0.95081203942629522,4.5825756949539365,nan,-1.5261506795942201,-0.42981831573151047 +r2p1_outward,0,0.57037671003940071,ref_tol1e-13_h0.125,0,0,0,-333.45070506871724,4589,2,32431,-0.30976827139480229,0,-0.95081208345133916,4.5825756949557528,nan,-1.5261506795946143,-0.42981831573151047 +r2p1_outward,0,0.57037671003940071,tol1e-11_h2,0,0,0,-333.45070071486469,1210,2,8778,-0.30976906608911198,0,-0.95081182454483582,4.5825756949192931,nan,-1.5261506795866686,-0.42981831573151047 +r2p1_outward,0,0.57037671003940071,tol1e-11_h8,0,0,0,-333.45070071488124,1169,2,8491,-0.30976906647032454,0,-0.95081182442063883,4.5825756949190151,nan,-1.5261506795866076,-0.42981831573151047 +r2p1_outward,1,0.57037681003940066,ref_tol1e-12_h0.25,0,0,0,-414.94010328784253,4263,2,30149,0.3336576345143959,0,0.94269432104487505,4.5825756949507124,nan,-1.5261506795930104,-0.42981805651739968 +r2p1_outward,1,0.57037681003940066,ref_tol1e-13_h0.125,0,0,0,-427.08073320404128,7560,1,53221,-0.91087431940825703,0,-0.41268386719442363,4.5825756949554943,nan,-1.5261506795940556,-0.42981805651739968 +r2p1_outward,1,0.57037681003940066,tol1e-11_h2,0,0,0,-406.06581257014602,2120,2,15141,0.88820167266587602,0,-0.45945379383953305,4.58257569487448,nan,-1.5261506795763404,-0.42981805651739968 +r2p1_outward,1,0.57037681003940066,tol1e-11_h8,0,0,0,-406.06581257016262,2078,2,14854,0.88817826517728515,0,-0.4594990416384655,4.5825756948742002,nan,-1.5261506795762789,-0.42981805651739968 +r2p1_outward,2,0.57037691003940061,ref_tol1e-12_h0.25,1,1,4294967295,-109.41561097431348,1950,1,13944,0,0,0,0,8.0000000000000195,7.5701822026967047,-0.42981779730331554 +r2p1_outward,2,0.57037691003940061,ref_tol1e-13_h0.125,1,1,4294967295,-109.41561022532379,3098,1,21980,0,0,0,0,8.000000000000016,7.5701822026967012,-0.42981779730331554 +r2p1_outward,2,0.57037691003940061,tol1e-11_h2,1,1,4294967295,-109.41561457850582,1228,1,8890,0,0,0,0,8.0000000000000195,7.5701822026967038,-0.42981779730331554 +r2p1_outward,2,0.57037691003940061,tol1e-11_h8,1,1,4294967295,-109.41561457850582,1228,1,8890,0,0,0,0,8.0000000000000195,7.5701822026967038,-0.42981779730331554 diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.log new file mode 100644 index 0000000..7066359 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/critical_ref.log @@ -0,0 +1,96 @@ +### building critical_ref_check ++ cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I/home/wyj/Code/C/GR_4d_raytracing/src /home/wyj/Code/C/GR_4d_raytracing/benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c /home/wyj/Code/C/GR_4d_raytracing/src/geodesic.c /home/wyj/Code/C/GR_4d_raytracing/src/asymptotic.c /home/wyj/Code/C/GR_4d_raytracing/src/asymptotic_schwarzschild.c /home/wyj/Code/C/GR_4d_raytracing/src/spacetime_common.c /home/wyj/Code/C/GR_4d_raytracing/src/observer.c -lm -o /tmp/opencode/step_bounds/critical_ref_check ++ echo '### command: critical_ref_check' +### command: critical_ref_check ++ /tmp/opencode/step_bounds/critical_ref_check +RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-142.17574796859728 steps=2081 rejected=0 rhs=14854 g=0 thr=8.0000000000000195 L=8.0658840965825682 L0=0.065884096582549498 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-142.17574845681202 steps=3332 rejected=0 rhs=23611 g=0 thr=8.0000000000000249 L=8.0658840965825735 L0=0.065884096582549498 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-142.17574172483768 steps=1304 rejected=0 rhs=9415 g=0 thr=8.0000000000000266 L=8.0658840965825753 L0=0.065884096582549498 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-142.17574172483768 steps=1304 rejected=0 rhs=9415 g=0 thr=8.0000000000000266 L=8.0658840965825753 L0=0.065884096582549498 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-225.85293500418322 steps=3746 rejected=0 rhs=26509 g=0 thr=8.0000000000000338 L=8.0658840955358357 L0=0.065884095535802364 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-238.90906331559862 steps=6388 rejected=0 rhs=45003 g=0 thr=8.000000000000016 L=8.065884095535818 L0=0.065884095535802364 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-212.59582609170599 steps=2180 rejected=0 rhs=15547 g=0 thr=8.0000000000000071 L=8.0658840955358091 L0=0.065884095535802364 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-212.59582609170599 steps=2180 rejected=0 rhs=15547 g=0 thr=8.0000000000000071 L=8.0658840955358091 L0=0.065884095535802364 nx=0 ny=0 nz=0 +RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-370.18047830118235 steps=2790 rejected=1 rhs=19831 g=1.0350983390130453 thr=nan L=-0.038385896473527227 L0=0.065884094489054593 nx=-0.35873499147611138 ny=-6.5068267720987458e-16 nz=-0.9334394495041628 +RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-370.1804778092 steps=4857 rejected=1 rhs=34300 g=1.035098339013504 thr=nan L=-0.038385896473972475 L0=0.065884094489054593 nx=-0.35873507984449715 ny=-6.5068265353608199e-16 nz=-0.93343941554284182 +RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-370.18048452857704 steps=1299 rejected=1 rhs=9387 g=1.0350983390054869 thr=nan L=-0.038385896466199748 L0=0.065884094489054593 nx=-0.3587338729380895 ny=-6.5068297686427377e-16 nz=-0.93343987937458439 +RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-370.1804845285946 steps=1257 rejected=1 rhs=9100 g=1.035098339005424 thr=nan L=-0.038385896466138665 L0=0.065884094489054593 nx=-0.35873387293809145 ny=-6.5068297686427416e-16 nz=-0.93343987937458361 +RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-211.027405235164 steps=2252 rejected=0 rhs=16051 g=0 thr=8.0000000000000266 L=8.0199767087115159 L0=0.01997670871149013 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-211.02740538583069 steps=3727 rejected=0 rhs=26376 g=0 thr=8.0000000000000142 L=8.0199767087115035 L0=0.01997670871149013 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-211.02740354768991 steps=1292 rejected=0 rhs=9331 g=0 thr=8.0000000000000533 L=8.0199767087115426 L0=0.01997670871149013 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-211.02740354768991 steps=1292 rejected=0 rhs=9331 g=0 thr=8.0000000000000533 L=8.0199767087115426 L0=0.01997670871149013 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-301.07740508539132 steps=4045 rejected=0 rhs=28602 g=0 thr=8.0000000000000018 L=8.0199767086106277 L0=0.019976708610626077 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-313.77734009633099 steps=6986 rejected=0 rhs=49189 g=0 thr=8.0000000000000018 L=8.0199767086106277 L0=0.019976708610626077 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-288.21364790845217 steps=2256 rejected=0 rhs=16079 g=0 thr=8.0000000000000639 L=8.0199767086106899 L0=0.019976708610626077 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-288.21364790845217 steps=2256 rejected=0 rhs=16079 g=0 thr=8.0000000000000639 L=8.0199767086106899 L0=0.019976708610626077 nx=0 ny=0 nz=0 +RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-439.31355204311825 steps=2969 rejected=0 rhs=21077 g=1.0101525445517785 thr=nan L=-0.013990814384499865 L0=0.019976708509762027 nx=-0.99110324423183982 ny=-3.1326762563025509e-16 nz=-0.13309530146899301 +RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-439.31355190087947 steps=5265 rejected=0 rhs=37149 g=1.010152544552184 thr=nan L=-0.013990814384902493 L0=0.019976708509762027 nx=-0.99110324787406912 ny=-3.132675617927086e-16 nz=-0.13309527434688151 +RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-439.31355372933643 steps=1291 rejected=1 rhs=9338 g=1.010152544545182 thr=nan L=-0.013990814377948021 L0=0.019976708509762027 nx=-0.99110320105936889 ny=-3.1326838231562615e-16 nz=-0.13309562295534913 +RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-439.31355372935258 steps=1250 rejected=1 rhs=9051 g=1.0101525445451216 thr=nan L=-0.013990814377887956 L0=0.019976708509762027 nx=-0.991103201059369 ny=-3.1326838231562231e-16 nz=-0.13309562295534744 +RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-333.45070432558816 steps=2639 rejected=1 rhs=18774 g=4.5825756949539365 thr=nan L=-1.5261506795942201 L0=-0.42981831573151047 nx=-0.30976840652656795 ny=0 nz=-0.95081203942629522 +RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-333.45070506871724 steps=4589 rejected=2 rhs=32431 g=4.5825756949557528 thr=nan L=-1.5261506795946143 L0=-0.42981831573151047 nx=-0.30976827139480229 ny=0 nz=-0.95081208345133916 +RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-333.45070071486469 steps=1210 rejected=2 rhs=8778 g=4.5825756949192931 thr=nan L=-1.5261506795866686 L0=-0.42981831573151047 nx=-0.30976906608911198 ny=0 nz=-0.95081182454483582 +RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-333.45070071488124 steps=1169 rejected=2 rhs=8491 g=4.5825756949190151 thr=nan L=-1.5261506795866076 L0=-0.42981831573151047 nx=-0.30976906647032454 ny=0 nz=-0.95081182442063883 +RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-414.94010328784253 steps=4263 rejected=2 rhs=30149 g=4.5825756949507124 thr=nan L=-1.5261506795930104 L0=-0.42981805651739968 nx=0.3336576345143959 ny=0 nz=0.94269432104487505 +RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-427.08073320404128 steps=7560 rejected=1 rhs=53221 g=4.5825756949554943 thr=nan L=-1.5261506795940556 L0=-0.42981805651739968 nx=-0.91087431940825703 ny=0 nz=-0.41268386719442363 +RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-406.06581257014602 steps=2120 rejected=2 rhs=15141 g=4.58257569487448 thr=nan L=-1.5261506795763404 L0=-0.42981805651739968 nx=0.88820167266587602 ny=0 nz=-0.45945379383953305 +RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-406.06581257016262 steps=2078 rejected=2 rhs=14854 g=4.5825756948742002 thr=nan L=-1.5261506795762789 L0=-0.42981805651739968 nx=0.88817826517728515 ny=0 nz=-0.4594990416384655 +RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-109.41561097431348 steps=1950 rejected=1 rhs=13944 g=0 thr=8.0000000000000195 L=7.5701822026967047 L0=-0.42981779730331554 nx=0 ny=0 nz=0 +RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-109.41561022532379 steps=3098 rejected=1 rhs=21980 g=0 thr=8.000000000000016 L=7.5701822026967012 L0=-0.42981779730331554 nx=0 ny=0 nz=0 +RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-109.41561457850582 steps=1228 rejected=1 rhs=8890 g=0 thr=8.0000000000000195 L=7.5701822026967038 L0=-0.42981779730331554 nx=0 ny=0 nz=0 +RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-109.41561457850582 steps=1228 rejected=1 rhs=8890 g=0 thr=8.0000000000000195 L=7.5701822026967038 L0=-0.42981779730331554 nx=0 ny=0 nz=0 +CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=4.8821473797033832e-07 dthr=-5.3290705182007514e-15 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-6.2437596000108897e-06 dthr=-7.1054273576010019e-15 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-6.2437596000108897e-06 dthr=-7.1054273576010019e-15 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=13.056128311415392 dthr=1.7763568394002505e-14 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-13.257108912477236 dthr=2.6645352591003757e-14 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-13.257108912477236 dthr=2.6645352591003757e-14 end=4294967295/4294967295 +CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=9.4669651562462453e-08 dgrel=4.4320103143036249e-13 dstop_t=-4.9198234819414211e-07 dthr=nan end=0/0 +CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=2.0539125955565396e-15 dgrel=6.0618177144533547e-14 dstop_t=1.7564616427989677e-11 dthr=nan end=0/0 +CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=1.198297077398556e-06 dgrel=7.3021588775645796e-12 dstop_t=6.2273946923596668e-06 dthr=nan end=0/0 +CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=1.1982970752891323e-06 dgrel=7.3627770547091131e-12 dstop_t=6.2274122569760948e-06 dthr=nan end=0/0 +CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=1.5066669334373728e-07 dthr=1.2434497875801753e-14 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-1.6874740822458989e-06 dthr=-2.6645352591003757e-14 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-1.6874740822458989e-06 dthr=-2.6645352591003757e-14 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=12.699935010939669 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-12.863757176939146 dthr=-6.2172489379008766e-14 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-12.863757176939146 dthr=-6.2172489379008766e-14 end=4294967295/4294967295 +CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=2.7365576305005138e-08 dgrel=4.0134562340199409e-13 dstop_t=-1.4223877542463015e-07 dthr=nan end=0/0 +CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=1.6930901125533633e-15 dgrel=5.9729998724833422e-14 dstop_t=1.6143530956469476e-11 dthr=nan end=0/0 +CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=3.243722235535319e-07 dgrel=6.5301097862402457e-12 dstop_t=1.6862181837495882e-06 dthr=nan end=0/0 +CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=3.2437222186044184e-07 dgrel=6.5900618295700042e-12 dstop_t=1.6862343272805447e-06 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=1.4212247739786624e-07 dgrel=3.9634961979118088e-13 dstop_t=7.4312907827334129e-07 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=4.0093378617100928e-10 dgrel=6.0618177144533547e-14 dstop_t=1.6541434888495132e-11 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=6.9368349486792499e-07 dgrel=7.5597306192776159e-12 dstop_t=-3.6107234677729139e-06 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=6.9408442870960736e-07 dgrel=7.6205708410270745e-12 dstop_t=-3.6107069263380254e-06 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=2.3363758065620761 dgrel=1.0434986208451846e-12 dstop_t=12.140629916198748 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=5.0943830155578499e-05 dgrel=6.106226635438361e-14 dstop_t=1.659827830735594e-11 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=1.7079955663413957 dgrel=1.6635359756378421e-11 dstop_t=-8.8742907176965105 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=1.7080465101715514 dgrel=1.6696422022732804e-11 dstop_t=-8.8742907176799122 dthr=nan end=0/0 +CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-7.4898969160130946e-07 dthr=3.5527136788005009e-15 end=4294967295/4294967295 +CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=3.6041923436869183e-06 dthr=0 end=4294967295/4294967295 +CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=3.6041923436869183e-06 dthr=0 end=4294967295/4294967295 +EXACT_CRITICAL_DIRECTION camera=r30_inward theta=0.16812289429621627 ref0=DARK/1/4294967295 ref1=DARK/1/4294967295 same=1 dn_ang=nan dstop_t=13.056128311415392 +EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r30_inward dn_ang=nan dstop_t=13.056128311415392 class_same=1 +EXACT_CRITICAL_DIRECTION camera=r100_inward theta=0.051461996376274736 ref0=DARK/1/4294967295 ref1=DARK/1/4294967295 same=1 dn_ang=nan dstop_t=12.699935010939669 +EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r100_inward dn_ang=nan dstop_t=12.699935010939669 class_same=1 +EXACT_CRITICAL_DIRECTION camera=r2p1_outward theta=0.57037681003940066 ref0=ESC/0/0 ref1=ESC/0/0 same=1 dn_ang=2.3363758065620761 dstop_t=12.140629916198748 +EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r2p1_outward dn_ang=2.3363758065620761 dstop_t=12.140629916198748 class_same=1 +NEIGHBOR_REFERENCE_CHECK camera=r30_inward dir=0 theta=0.16812279429621627 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=4.8821473797033832e-07 +NEIGHBOR_REFERENCE_CHECK camera=r30_inward dir=2 theta=0.16812299429621627 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=9.4669651562462453e-08 dgrel=4.4320103143036249e-13 dstop_t=-4.9198234819414211e-07 +NEIGHBOR_REFERENCE_CHECK camera=r100_inward dir=0 theta=0.051461896376274734 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=1.5066669334373728e-07 +NEIGHBOR_REFERENCE_CHECK camera=r100_inward dir=2 theta=0.051462096376274739 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=2.7365576305005138e-08 dgrel=4.0134562340199409e-13 dstop_t=-1.4223877542463015e-07 +NEIGHBOR_REFERENCE_CHECK camera=r2p1_outward dir=0 theta=0.57037671003940071 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=1.4212247739786624e-07 dgrel=3.9634961979118088e-13 dstop_t=7.4312907827334129e-07 +NEIGHBOR_REFERENCE_CHECK camera=r2p1_outward dir=2 theta=0.57037691003940061 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=-7.4898969160130946e-07 +CRITICAL_REFERENCE_CHECK status=0 +CRITICAL_REF_DONE + +real 0m0.418s +user 0m0.402s +sys 0m0.016s ++ set +x diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/expA.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/expA.log new file mode 100644 index 0000000..b9fba75 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/expA.log @@ -0,0 +1,271 @@ +### command: a_public schwarzschild ++ /tmp/opencode/step_bounds/a_public schwarzschild +REF r30_inward dir=0 theta=0 outcome=1 reason=1 stop_t=-69.5728394 steps=531 rejected=0 rhs=4004 g=0 thr=8 +REF r30_inward dir=1 theta=3.14159 outcome=0 reason=0 stop_t=-226 steps=905 rejected=0 rhs=6629 g=1.03509833901 thr=nan +REF r30_inward dir=2 theta=1.5708 outcome=0 reason=0 stop_t=-256.70592 steps=1028 rejected=0 rhs=7490 g=1.03509833901 thr=nan +REF r30_inward dir=3 theta=0.168123 outcome=1 reason=1 stop_t=-225.852935 steps=3746 rejected=0 rhs=26509 g=0 thr=8 +REF r30_inward dir=4 theta=0.167123 outcome=1 reason=1 stop_t=-94.2779103 steps=1127 rejected=0 rhs=8176 g=0 thr=8 +REF r30_inward dir=5 theta=0.169123 outcome=0 reason=0 stop_t=-322.366447 steps=1834 rejected=0 rhs=13132 g=1.03509833901 thr=nan +REF r30_inward dir=6 theta=0.168113 outcome=1 reason=1 stop_t=-118.245807 steps=1606 rejected=0 rhs=11529 g=0 thr=8 +REF r30_inward dir=7 theta=0.168133 outcome=0 reason=0 stop_t=-346.252134 steps=2312 rejected=1 rhs=16485 g=1.03509833901 thr=nan +REF r30_inward dir=8 theta=0.168123 outcome=1 reason=1 stop_t=-142.175748 steps=2081 rejected=0 rhs=14854 g=0 thr=8 +REF r30_inward dir=9 theta=0.168123 outcome=0 reason=0 stop_t=-370.180478 steps=2790 rejected=1 rhs=19831 g=1.03509833901 thr=nan +REF r30_inward dir=10 theta=0.218123 outcome=0 reason=0 stop_t=-302.584521 steps=1409 rejected=1 rhs=10164 g=1.03509833901 thr=nan +REF r30_inward dir=11 theta=0.118123 outcome=1 reason=1 stop_t=-73.7563113 steps=665 rejected=0 rhs=4942 g=0 thr=8 +REF r100_inward dir=0 theta=0 outcome=1 reason=1 stop_t=-144.299101 steps=830 rejected=0 rhs=6097 g=0 thr=8 +REF r100_inward dir=1 theta=3.14159 outcome=0 reason=0 stop_t=-156 steps=625 rejected=0 rhs=4669 g=1.01015254455 thr=nan +REF r100_inward dir=2 theta=1.5708 outcome=0 reason=0 stop_t=-237.678408 steps=952 rejected=0 rhs=6958 g=1.01015254455 thr=nan +REF r100_inward dir=3 theta=0.051462 outcome=1 reason=1 stop_t=-301.077405 steps=4045 rejected=0 rhs=28602 g=0 thr=8 +REF r100_inward dir=4 theta=0.050462 outcome=1 reason=1 stop_t=-163.061409 steps=1292 rejected=0 rhs=9331 g=0 thr=8 +REF r100_inward dir=5 theta=0.052462 outcome=0 reason=0 stop_t=-391.57927 steps=2014 rejected=1 rhs=14399 g=1.01015254455 thr=nan +REF r100_inward dir=6 theta=0.051452 outcome=1 reason=1 stop_t=-187.095903 steps=1773 rejected=0 rhs=12698 g=0 thr=8 +REF r100_inward dir=7 theta=0.051472 outcome=0 reason=0 stop_t=-415.386875 steps=2493 rejected=0 rhs=17745 g=1.01015254455 thr=nan +REF r100_inward dir=8 theta=0.0514619 outcome=1 reason=1 stop_t=-211.027405 steps=2252 rejected=0 rhs=16051 g=0 thr=8 +REF r100_inward dir=9 theta=0.0514621 outcome=0 reason=0 stop_t=-439.313552 steps=2969 rejected=0 rhs=21077 g=1.01015254455 thr=nan +REF r100_inward dir=10 theta=0.101462 outcome=0 reason=0 stop_t=-372.348056 steps=1571 rejected=0 rhs=11291 g=1.01015254455 thr=nan +REF r100_inward dir=11 theta=0.001462 outcome=1 reason=1 stop_t=-144.304446 steps=830 rejected=0 rhs=6097 g=0 thr=8 +REF r2p1_outward dir=0 theta=0 outcome=0 reason=0 stop_t=-253.9 steps=1120 rejected=1 rhs=8141 g=4.58257569496 thr=nan +REF r2p1_outward dir=1 theta=3.14159 outcome=1 reason=1 stop_t=-32.2468847 steps=186 rejected=0 rhs=1589 g=0 thr=8 +REF r2p1_outward dir=2 theta=1.5708 outcome=1 reason=1 stop_t=-32.7725901 steps=316 rejected=1 rhs=2506 g=0 thr=8 +REF r2p1_outward dir=3 theta=0.570377 outcome=0 reason=0 stop_t=-414.940103 steps=4263 rejected=2 rhs=30149 g=4.58257569495 thr=nan +REF r2p1_outward dir=4 theta=0.569377 outcome=0 reason=0 stop_t=-285.577403 steps=1686 rejected=1 rhs=12103 g=4.58257569496 thr=nan +REF r2p1_outward dir=5 theta=0.571377 outcome=1 reason=1 stop_t=-61.5813727 steps=995 rejected=1 rhs=7259 g=0 thr=8 +REF r2p1_outward dir=6 theta=0.570367 outcome=0 reason=0 stop_t=-309.521289 steps=2162 rejected=2 rhs=15442 g=4.58257569495 thr=nan +REF r2p1_outward dir=7 theta=0.570387 outcome=1 reason=1 stop_t=-85.4867841 steps=1474 rejected=1 rhs=10612 g=0 thr=8 +REF r2p1_outward dir=8 theta=0.570377 outcome=0 reason=0 stop_t=-333.450704 steps=2639 rejected=1 rhs=18774 g=4.58257569495 thr=nan +REF r2p1_outward dir=9 theta=0.570377 outcome=1 reason=1 stop_t=-109.415611 steps=1950 rejected=1 rhs=13944 g=0 thr=8 +REF r2p1_outward dir=10 theta=0.620377 outcome=1 reason=1 stop_t=-41.9883684 steps=585 rejected=1 rhs=4389 g=0 thr=8 +REF r2p1_outward dir=11 theta=0.520377 outcome=0 reason=0 stop_t=-264.96182 steps=1303 rejected=2 rhs=9429 g=4.58257569496 thr=nan +REF r1p5_freefall dir=0 theta=0 outcome=1 reason=1 stop_t=-30.6580907 steps=231 rejected=1 rhs=1911 g=0 thr=8 +REF r1p5_freefall dir=1 theta=1.5708 outcome=0 reason=0 stop_t=-255.249558 steps=1130 rejected=1 rhs=8211 g=1 thr=nan +REF r1p5_freefall dir=2 theta=3.14159 outcome=0 reason=0 stop_t=-254.5 steps=1143 rejected=1 rhs=8302 g=0.464101615138 thr=nan +REF r1p5_freefall dir=3 theta=2.35619 outcome=0 reason=0 stop_t=-254.612997 steps=1127 rejected=1 rhs=8190 g=0.550510257217 thr=nan +SUMMARY upper r30_inward upper=0.25 min=1e-12 n=12 mismatch=0 max_dn_ang=8.66705e-05 max_dgrel=2.2616e-09 max_dstopT=35.0811 sum_rhs=76510 esc=6 dark=6 unres=0 inc=0 wall=0.041s +SUMMARY upper r30_inward upper=0.5 min=1e-12 n=12 mismatch=0 max_dn_ang=0.00018656 max_dgrel=2.26833e-09 max_dstopT=39.0614 sum_rhs=49063 esc=6 dark=6 unres=0 inc=0 wall=0.027s +SUMMARY upper r30_inward upper=1 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000280143 max_dgrel=2.2658e-09 max_dstopT=41.1726 sum_rhs=36974 esc=6 dark=6 unres=0 inc=0 wall=0.020s +SUMMARY upper r30_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY upper r30_inward upper=4 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24952e-09 max_dstopT=41.2044 sum_rhs=29617 esc=6 dark=6 unres=0 inc=0 wall=0.016s +SUMMARY upper r30_inward upper=8 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.25767e-09 max_dstopT=41.2044 sum_rhs=28714 esc=6 dark=6 unres=0 inc=0 wall=0.016s +SUMMARY upper r30_inward upper=16 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26556e-09 max_dstopT=41.2044 sum_rhs=28455 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r30_inward upper=32 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26605e-09 max_dstopT=41.2044 sum_rhs=28434 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r30_inward upper=64 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26605e-09 max_dstopT=41.2044 sum_rhs=28434 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r100_inward upper=0.25 min=1e-12 n=12 mismatch=0 max_dn_ang=2.60363e-05 max_dgrel=2.04765e-09 max_dstopT=35.2041 sum_rhs=93464 esc=6 dark=6 unres=0 inc=0 wall=0.050s +SUMMARY upper r100_inward upper=0.5 min=1e-12 n=12 mismatch=0 max_dn_ang=5.69583e-05 max_dgrel=2.05623e-09 max_dstopT=39.2691 sum_rhs=56595 esc=6 dark=6 unres=0 inc=0 wall=0.030s +SUMMARY upper r100_inward upper=1 min=1e-12 n=12 mismatch=0 max_dn_ang=8.34774e-05 max_dgrel=2.05088e-09 max_dstopT=41.2546 sum_rhs=39788 esc=6 dark=6 unres=0 inc=0 wall=0.022s +SUMMARY upper r100_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY upper r100_inward upper=4 min=1e-12 n=12 mismatch=0 max_dn_ang=8.08898e-05 max_dgrel=2.02847e-09 max_dstopT=41.091 sum_rhs=29393 esc=6 dark=6 unres=0 inc=0 wall=0.016s +SUMMARY upper r100_inward upper=8 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.03431e-09 max_dstopT=41.0553 sum_rhs=28427 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r100_inward upper=16 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04195e-09 max_dstopT=41.0553 sum_rhs=28189 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r100_inward upper=32 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04297e-09 max_dstopT=41.0553 sum_rhs=28182 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r100_inward upper=64 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04297e-09 max_dstopT=41.0553 sum_rhs=28182 esc=6 dark=6 unres=0 inc=0 wall=0.015s +SUMMARY upper r2p1_outward upper=0.25 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287223 max_dgrel=2.66126e-09 max_dstopT=263.145 sum_rhs=62258 esc=5 dark=7 unres=0 inc=0 wall=0.034s +SUMMARY upper r2p1_outward upper=0.5 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.64443e-09 max_dstopT=263.145 sum_rhs=42490 esc=5 dark=7 unres=0 inc=0 wall=0.023s +SUMMARY upper r2p1_outward upper=1 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.61531e-09 max_dstopT=263.145 sum_rhs=33544 esc=5 dark=7 unres=0 inc=0 wall=0.019s +SUMMARY upper r2p1_outward upper=2 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY upper r2p1_outward upper=4 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.59774e-09 max_dstopT=263.145 sum_rhs=27580 esc=5 dark=7 unres=0 inc=0 wall=0.015s +SUMMARY upper r2p1_outward upper=8 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.60591e-09 max_dstopT=263.145 sum_rhs=26831 esc=5 dark=7 unres=0 inc=0 wall=0.015s +SUMMARY upper r2p1_outward upper=16 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287223 max_dgrel=2.61374e-09 max_dstopT=263.145 sum_rhs=26614 esc=5 dark=7 unres=0 inc=0 wall=0.015s +SUMMARY upper r2p1_outward upper=32 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.6148e-09 max_dstopT=263.145 sum_rhs=26607 esc=5 dark=7 unres=0 inc=0 wall=0.015s +SUMMARY upper r2p1_outward upper=64 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.6148e-09 max_dstopT=263.145 sum_rhs=26607 esc=5 dark=7 unres=0 inc=0 wall=0.015s +SUMMARY upper r1p5_freefall upper=0.25 min=1e-12 n=4 mismatch=0 max_dn_ang=1.00222e-10 max_dgrel=3.16466e-10 max_dstopT=2.6078e-08 sum_rhs=23597 esc=3 dark=1 unres=0 inc=0 wall=0.014s +SUMMARY upper r1p5_freefall upper=0.5 min=1e-12 n=4 mismatch=0 max_dn_ang=8.87544e-11 max_dgrel=3.65847e-10 max_dstopT=3.08873e-08 sum_rhs=12747 esc=3 dark=1 unres=0 inc=0 wall=0.008s +SUMMARY upper r1p5_freefall upper=1 min=1e-12 n=4 mismatch=0 max_dn_ang=8.18746e-11 max_dgrel=3.81078e-10 max_dstopT=2.81655e-08 sum_rhs=7553 esc=3 dark=1 unres=0 inc=0 wall=0.005s +SUMMARY upper r1p5_freefall upper=2 min=1e-12 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY upper r1p5_freefall upper=4 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1566e-11 max_dgrel=3.6318e-10 max_dstopT=2.34608e-08 sum_rhs=4039 esc=3 dark=1 unres=0 inc=0 wall=0.002s +SUMMARY upper r1p5_freefall upper=8 min=1e-12 n=4 mismatch=0 max_dn_ang=8.15091e-11 max_dgrel=3.54659e-10 max_dstopT=2.12507e-08 sum_rhs=3591 esc=3 dark=1 unres=0 inc=0 wall=0.002s +SUMMARY upper r1p5_freefall upper=16 min=1e-12 n=4 mismatch=0 max_dn_ang=8.14607e-11 max_dgrel=3.46852e-10 max_dstopT=1.92511e-08 sum_rhs=3465 esc=3 dark=1 unres=0 inc=0 wall=0.002s +SUMMARY upper r1p5_freefall upper=32 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1455e-11 max_dgrel=3.45979e-10 max_dstopT=1.94793e-08 sum_rhs=3458 esc=3 dark=1 unres=0 inc=0 wall=0.002s +SUMMARY upper r1p5_freefall upper=64 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1455e-11 max_dgrel=3.45979e-10 max_dstopT=1.94793e-08 sum_rhs=3458 esc=3 dark=1 unres=0 inc=0 wall=0.002s +SUMMARY min r30_inward upper=2 min=0.1 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=0.01 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=0.001 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=0.0001 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-05 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-06 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-08 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-10 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r30_inward upper=2 min=1e-14 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s +SUMMARY min r100_inward upper=2 min=0.1 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=0.01 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=0.001 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=0.0001 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-05 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-06 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-08 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-10 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r100_inward upper=2 min=1e-14 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s +SUMMARY min r2p1_outward upper=2 min=0.1 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=0.01 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=0.001 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.017s +SUMMARY min r2p1_outward upper=2 min=0.0001 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.017s +SUMMARY min r2p1_outward upper=2 min=1e-05 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=1e-06 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=1e-08 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=1e-10 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r2p1_outward upper=2 min=1e-14 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s +SUMMARY min r1p5_freefall upper=2 min=0.1 n=4 mismatch=1 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=254.3 sum_rhs=3654 esc=2 dark=1 unres=0 inc=1 wall=0.002s +SUMMARY min r1p5_freefall upper=2 min=0.01 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=0.001 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=0.0001 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-05 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-06 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-08 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-10 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-12 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +SUMMARY min r1p5_freefall upper=2 min=1e-14 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s +RK4 r30_inward dir=8 theta=0.168123 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1 +RK4 r30_inward dir=9 theta=0.168123 o01=0/0 o005=0/0 hhalve_dn=1.89584e-07 hhalve_dgrel=7.32747e-15 vsdp_dn=7.95167e-08 vsdp_dgrel=4.66294e-13 vsdp_class=1 +RK4 r30_inward dir=6 theta=0.168113 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1 +RK4 r30_inward dir=7 theta=0.168133 o01=0/0 o005=0/0 hhalve_dn=1.73965e-09 hhalve_dgrel=7.9492e-14 vsdp_dn=7.86496e-10 vsdp_dgrel=2.6934e-13 vsdp_class=1 +RK4 r2p1_outward dir=8 theta=0.570377 o01=0/0 o005=0/0 hhalve_dn=1.03961e-05 hhalve_dgrel=1.66234e-12 vsdp_dn=8.49321e-07 vsdp_dgrel=5.46896e-13 vsdp_class=1 +RK4 r2p1_outward dir=9 theta=0.570377 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1 +RK4 r2p1_outward dir=6 theta=0.570367 o01=0/0 o005=0/0 hhalve_dn=1.03971e-07 hhalve_dgrel=1.61182e-12 vsdp_dn=8.47692e-09 vsdp_dgrel=3.02647e-13 vsdp_class=1 +RK4 r2p1_outward dir=7 theta=0.570387 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1 +TOTAL_RAYS 816 + +real 0m2.428s +user 0m2.423s +sys 0m0.004s ++ set +x +### command: a_public minkowski ++ /tmp/opencode/step_bounds/a_public minkowski +SUMMARY mink upper=1 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=1 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=1 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=4 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=4 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=4 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=16 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=16 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=16 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=64 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=64 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=64 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=256 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=256 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +SUMMARY mink upper=256 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0 +TOTAL_RAYS 186 + +real 0m0.013s +user 0m0.013s +sys 0m0.000s ++ set +x +### command: a_public alcubierre ++ /tmp/opencode/step_bounds/a_public alcubierre +REF alc_v3_s1 dir=0 theta=nan outcome=0 reason=0 stop_t=-16.4338937 steps=347 rejected=1 rhs=2730 g=1.3 thr=nan +REF alc_v3_s1 dir=1 theta=nan outcome=0 reason=0 stop_t=-29.3767055 steps=597 rejected=1 rhs=4480 g=0.7 thr=nan +REF alc_v3_s1 dir=2 theta=nan outcome=0 reason=0 stop_t=-20.2190145 steps=414 rejected=0 rhs=3192 g=1 thr=nan +REF alc_v3_s1 dir=3 theta=nan outcome=0 reason=0 stop_t=-20.2190145 steps=414 rejected=0 rhs=3192 g=1 thr=nan +REF alc_v3_s1 dir=4 theta=nan outcome=0 reason=0 stop_t=-17.6897755 steps=367 rejected=0 rhs=2863 g=1.17815368048 thr=nan +REF alc_v3_s1 dir=5 theta=nan outcome=0 reason=0 stop_t=-24.329102 steps=497 rejected=1 rhs=3780 g=0.822613315483 thr=nan +REF alc_v3_s10 dir=0 theta=nan outcome=0 reason=0 stop_t=-2.53543426 steps=523 rejected=3 rhs=3976 g=1.3 thr=nan +REF alc_v3_s10 dir=1 theta=nan outcome=0 reason=0 stop_t=-3.84949982 steps=775 rejected=0 rhs=5719 g=0.7 thr=nan +REF alc_v3_s10 dir=2 theta=nan outcome=0 reason=0 stop_t=-2.94282517 steps=589 rejected=0 rhs=4417 g=1 thr=nan +REF alc_v3_s10 dir=3 theta=nan outcome=0 reason=0 stop_t=-2.94282517 steps=589 rejected=0 rhs=4417 g=1 thr=nan +REF alc_v3_s10 dir=4 theta=nan outcome=0 reason=0 stop_t=-2.67090915 steps=543 rejected=2 rhs=4109 g=1.17959135733 thr=nan +REF alc_v3_s10 dir=5 theta=nan outcome=0 reason=0 stop_t=-3.3698049 steps=677 rejected=0 rhs=5033 g=0.820579506843 thr=nan +REF alc_v9_s1 dir=0 theta=nan outcome=0 reason=0 stop_t=-11.5627491 steps=283 rejected=1 rhs=2282 g=1.9 thr=nan +REF alc_v9_s1 dir=1 theta=nan outcome=0 reason=0 stop_t=-189.149503 steps=3825 rejected=1 rhs=27069 g=0.1 thr=nan +REF alc_v9_s1 dir=2 theta=nan outcome=0 reason=0 stop_t=-16.1619146 steps=351 rejected=4 rhs=2772 g=1 thr=nan +REF alc_v9_s1 dir=3 theta=nan outcome=0 reason=0 stop_t=-16.1619146 steps=351 rejected=4 rhs=2772 g=1 thr=nan +REF alc_v9_s1 dir=4 theta=nan outcome=0 reason=0 stop_t=-13.0192142 steps=308 rejected=1 rhs=2457 g=1.5041731473 thr=nan +REF alc_v9_s1 dir=5 theta=nan outcome=0 reason=0 stop_t=-22.987334 steps=491 rejected=1 rhs=3738 g=0.54079971046 thr=nan +REF alc_v9_s10 dir=0 theta=nan outcome=0 reason=0 stop_t=-2.03742978 steps=452 rejected=5 rhs=3493 g=1.9 thr=nan +REF alc_v9_s10 dir=1 theta=nan outcome=0 reason=0 stop_t=-19.9638367 steps=4013 rejected=0 rhs=28385 g=0.1 thr=nan +REF alc_v9_s10 dir=2 theta=nan outcome=0 reason=0 stop_t=-2.62069451 steps=525 rejected=0 rhs=3969 g=1 thr=nan +REF alc_v9_s10 dir=3 theta=nan outcome=0 reason=0 stop_t=-2.62069451 steps=525 rejected=0 rhs=3969 g=1 thr=nan +REF alc_v9_s10 dir=4 theta=nan outcome=0 reason=0 stop_t=-2.20704191 steps=469 rejected=3 rhs=3598 g=1.5316539409 thr=nan +REF alc_v9_s10 dir=5 theta=nan outcome=0 reason=0 stop_t=-3.67922549 steps=742 rejected=0 rhs=5488 g=0.483265790771 thr=nan +REF alc_v9_s100 dir=0 theta=nan outcome=0 reason=0 stop_t=-1.10374298 steps=2252 rejected=5 rhs=16093 g=1.9 thr=nan +REF alc_v9_s100 dir=1 theta=nan outcome=0 reason=0 stop_t=-2.89638367 steps=5813 rejected=0 rhs=40978 g=0.1 thr=nan +REF alc_v9_s100 dir=2 theta=nan outcome=0 reason=0 stop_t=-1.18800796 steps=2377 rejected=0 rhs=16926 g=1 thr=nan +REF alc_v9_s100 dir=3 theta=nan outcome=0 reason=0 stop_t=-1.18800796 steps=2377 rejected=0 rhs=16926 g=1 thr=nan +REF alc_v9_s100 dir=4 theta=nan outcome=0 reason=0 stop_t=-1.12678339 steps=2281 rejected=2 rhs=16275 g=1.53910006629 thr=nan +REF alc_v9_s100 dir=5 theta=nan outcome=0 reason=0 stop_t=-1.36694611 steps=2740 rejected=0 rhs=19474 g=0.462933441354 thr=nan +SUMMARY upper alc_v3_s1 upper=0.05 min=0.0001 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s +SUMMARY upper alc_v3_s1 upper=0.05 min=1e-08 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s +SUMMARY upper alc_v3_s1 upper=0.05 min=1e-12 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s +SUMMARY upper alc_v3_s1 upper=0.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v3_s1 upper=0.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v3_s1 upper=0.4 min=0.0001 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.4 min=1e-08 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.4 min=1e-12 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.8 min=0.0001 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.8 min=1e-08 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=0.8 min=1e-12 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=1.6 min=0.0001 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=1.6 min=1e-08 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=1.6 min=1e-12 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=3.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=3.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s1 upper=3.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.005 min=0.0001 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s +SUMMARY upper alc_v3_s10 upper=0.005 min=1e-08 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s +SUMMARY upper alc_v3_s10 upper=0.005 min=1e-12 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s +SUMMARY upper alc_v3_s10 upper=0.02 min=0.0001 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v3_s10 upper=0.02 min=1e-08 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v3_s10 upper=0.02 min=1e-12 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v3_s10 upper=0.04 min=0.0001 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.04 min=1e-08 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.04 min=1e-12 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.08 min=0.0001 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.08 min=1e-08 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.08 min=1e-12 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.16 min=0.0001 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.16 min=1e-08 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.16 min=1e-12 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.32 min=0.0001 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.32 min=1e-08 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v3_s10 upper=0.32 min=1e-12 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=0.05 min=0.0001 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s +SUMMARY upper alc_v9_s1 upper=0.05 min=1e-08 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s +SUMMARY upper alc_v9_s1 upper=0.05 min=1e-12 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s +SUMMARY upper alc_v9_s1 upper=0.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s1 upper=0.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s1 upper=0.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s1 upper=0.4 min=0.0001 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s1 upper=0.4 min=1e-08 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s1 upper=0.4 min=1e-12 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s1 upper=0.8 min=0.0001 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=0.8 min=1e-08 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=0.8 min=1e-12 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=1.6 min=0.0001 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=1.6 min=1e-08 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=1.6 min=1e-12 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=3.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=3.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s1 upper=3.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.005 min=0.0001 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s +SUMMARY upper alc_v9_s10 upper=0.005 min=1e-08 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s +SUMMARY upper alc_v9_s10 upper=0.005 min=1e-12 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s +SUMMARY upper alc_v9_s10 upper=0.02 min=0.0001 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s10 upper=0.02 min=1e-08 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s10 upper=0.02 min=1e-12 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s +SUMMARY upper alc_v9_s10 upper=0.04 min=0.0001 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s10 upper=0.04 min=1e-08 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s10 upper=0.04 min=1e-12 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s +SUMMARY upper alc_v9_s10 upper=0.08 min=0.0001 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.08 min=1e-08 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.08 min=1e-12 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.16 min=0.0001 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.16 min=1e-08 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.16 min=1e-12 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.32 min=0.0001 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.32 min=1e-08 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY upper alc_v9_s10 upper=0.32 min=1e-12 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=0.0001 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-05 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-06 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-08 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-12 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-14 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s +TOTAL_RAYS 498 + +real 0m0.264s +user 0m0.254s +sys 0m0.010s ++ set +x diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/expB.log b/benchmarks/adaptive_step_bounds_2026-10-05/results/expB.log new file mode 100644 index 0000000..008738e --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/expB.log @@ -0,0 +1,33 @@ +### command: b_actual ++ /tmp/opencode/step_bounds/b_actual +B r30_inward dir=0 ref=1 refstop=-69.5728394 span=69.5728 T=20 target=-20 steps=17 reached=1 term=-1 h=[0.1,1.72575] first=0.1 last=0.839319 boundary=1 rhs=119 rej=0 nullmax=3.01e-12 final_t=-20 wall=0.0001s +B r30_inward dir=1 ref=0 refstop=-226 span=226 T=20 target=-20 steps=13 reached=1 term=-1 h=[0.1,2] first=0.1 last=0.123959 boundary=1 rhs=91 rej=0 nullmax=8.69e-12 final_t=-20 wall=0.0001s +B r30_inward dir=2 ref=1 refstop=-142.175748 span=142.176 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40832] first=0.1 last=0.637699 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s +B r30_inward dir=3 ref=0 refstop=-370.180478 span=370.18 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40832] first=0.1 last=0.6377 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s +B r30_inward dir=4 ref=1 refstop=-118.245807 span=118.246 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40833] first=0.1 last=0.637634 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s +B r30_inward dir=5 ref=0 refstop=-346.252134 span=346.252 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40831] first=0.1 last=0.637765 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s +B r100_inward dir=0 ref=1 refstop=-144.299101 span=144.299 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.45e-14 final_t=-20 wall=0.0001s +B r100_inward dir=1 ref=0 refstop=-156 span=156 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=7.66e-15 final_t=-20 wall=0.0001s +B r100_inward dir=2 ref=1 refstop=-301.077405 span=301.077 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.4e-14 final_t=-20 wall=0.0001s +B r100_inward dir=3 ref=1 refstop=-211.027405 span=211.027 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.32e-14 final_t=-20 wall=0.0001s +B r2p1_outward dir=0 ref=0 refstop=-253.9 span=253.9 T=20 target=-20 steps=46 reached=1 term=-1 h=[0.1,1.0708] first=0.1 last=1.06594 boundary=1 rhs=322 rej=0 nullmax=6.27e-11 final_t=-20 wall=0.0002s +B r2p1_outward dir=1 ref=0 refstop=-414.940103 span=414.94 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211533] first=0.1 last=0.192781 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s +B r2p1_outward dir=2 ref=0 refstop=-333.450704 span=333.451 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211533] first=0.1 last=0.192739 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s +B r2p1_outward dir=3 ref=1 refstop=-109.415611 span=109.416 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211532] first=0.1 last=0.192823 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s +B r2p1_outward dir=4 ref=1 refstop=-32.2468847 span=32.2469 T=20 target=-20 steps=37 reached=1 term=-1 h=[0.1,0.899361] first=0.1 last=0.332712 boundary=1 rhs=259 rej=0 nullmax=2.99e-11 final_t=-20 wall=0.0002s +B r1p5_freefall dir=0 ref=1 refstop=-30.6580907 span=30.6581 T=20 target=-20 steps=49 reached=1 term=-1 h=[0.1,0.695968] first=0.1 last=0.106174 boundary=1 rhs=343 rej=0 nullmax=4.94e-10 final_t=-20 wall=0.0002s +B r1p5_freefall dir=1 ref=0 refstop=-254.5 span=254.5 T=20 target=-20 steps=52 reached=1 term=-1 h=[0.0920967,1.08419] first=0.1 last=0.22147 boundary=1 rhs=364 rej=0 nullmax=4.03e-11 final_t=-20 wall=0.0002s +B mink_moving dir=0 ref=0 refstop=-51.2640254 span=51.264 T=20 target=-20 steps=3 reached=1 term=-1 h=[1,14] first=1 last=14 boundary=1 rhs=21 rej=0 nullmax=2.22e-16 final_t=-20 wall=0.0000s +B mink_moving dir=1 ref=0 refstop=-71.5846741 span=71.5847 T=20 target=-20 steps=3 reached=1 term=-1 h=[1,14] first=1 last=14 boundary=1 rhs=21 rej=0 nullmax=0 final_t=-20 wall=0.0000s +B alc_v3_s1 dir=0 ref=0 refstop=-16.4338937 span=16.4339 T=16.4339 target=-16.4339 steps=56 reached=1 term=-1 h=[0.05,0.4] first=0.05 last=0.203538 boundary=1 rhs=406 rej=2 nullmax=0 final_t=-16.4338937 wall=0.0001s +B alc_v3_s1 dir=1 ref=0 refstop=-29.3767055 span=29.3767 T=20 target=-20 steps=62 reached=1 term=-1 h=[0.05,0.4] first=0.05 last=0.27784 boundary=1 rhs=441 rej=1 nullmax=0 final_t=-20 wall=0.0001s +B alc_v9_s10 dir=0 ref=0 refstop=-2.03742978 span=2.03743 T=2.03743 target=-2.03743 steps=77 reached=1 term=-1 h=[0.005,0.04] first=0.005 last=0.0388523 boundary=1 rhs=588 rej=7 nullmax=0 final_t=-2.03742978 wall=0.0001s +B alc_v9_s10 dir=1 ref=0 refstop=-19.9638367 span=19.9638 T=19.9638 target=-19.9638 steps=521 reached=1 term=-1 h=[0.005,0.04] first=0.005 last=0.0365282 boundary=1 rhs=3682 rej=5 nullmax=2.22e-16 final_t=-19.9638367 wall=0.0009s +B alc_v9_s100 dir=0 ref=0 refstop=-1.10374298 span=1.10374 T=1.10374 target=-1.10374 steps=302 reached=1 term=-1 h=[0.0005,0.004] first=0.0005 last=0.00388798 boundary=1 rhs=2163 rej=7 nullmax=0 final_t=-1.10374298 wall=0.0005s +B alc_v9_s100 dir=1 ref=0 refstop=-2.89638367 span=2.89638 T=2.89638 target=-2.89638 steps=746 reached=1 term=-1 h=[0.0005,0.004] first=0.0005 last=0.00365282 boundary=1 rhs=5257 rej=5 nullmax=2.22e-16 final_t=-2.89638367 wall=0.0012s +TOTAL_OBSERVED_STEPS 2427 + +real 0m0.152s +user 0m0.145s +sys 0m0.006s ++ set +x diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_reference_summary.json b/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_reference_summary.json new file mode 100644 index 0000000..9394541 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_reference_summary.json @@ -0,0 +1,142 @@ +{ + "r100_tight_reference": { + "vertices": 3158, + "triangles": 6188, + "outcomes": { + "0": 2906, + "1": 252 + }, + "reasons": { + "0": 2906, + "1": 252 + }, + "accepted": 2908050, + "rejected": 1963, + "rhs": 21294763, + "accepted_percentiles": { + "0": 659, + "0.5": 885, + "0.9": 1166, + "0.99": 1410, + "1": 1609 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 0, + "0.9": 2, + "0.99": 3, + "1": 4 + }, + "rhs_percentiles": { + "0": 4900, + "0.5": 6496, + "0.9": 8463, + "0.99": 10171, + "1": 11564 + }, + "sha256": "a4f7def4212a7dae2b6290fd8635a5f1b087e11b0f8043e60e48fec71ecdb6e4", + "wall_seconds": 2.6278538939077407 + }, + "r100_ref_vs_0.5": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 6.236425088081268e-08, + "max_logg_difference": 8.506182824080666e-10 + }, + "r100_ref_vs_2": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 9.216337364737666e-08, + "max_logg_difference": 8.199086565935376e-10 + }, + "r100_ref_vs_8": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 8.743723551600822e-08, + "max_logg_difference": 8.257244662329688e-10 + }, + "r100_ref_vs_32": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 8.743704837242435e-08, + "max_logg_difference": 8.34318713074933e-10 + }, + "r2p1_tight_reference": { + "vertices": 5496, + "triangles": 10816, + "outcomes": { + "1": 1336, + "0": 4160 + }, + "reasons": { + "1": 1336, + "0": 4160 + }, + "accepted": 4515098, + "rejected": 8672, + "rhs": 33269502, + "accepted_percentiles": { + "0": 419, + "0.5": 805, + "0.9": 1034, + "0.99": 1253, + "1": 1860 + }, + "rejected_percentiles": { + "0": 1, + "0.5": 2, + "0.9": 2, + "0.99": 3, + "1": 3 + }, + "rhs_percentiles": { + "0": 3227, + "0.5": 5936, + "0.9": 7546, + "0.99": 9072, + "1": 13328 + }, + "sha256": "a81aac81cff2891627e21c22998ceb51d65ef779bb013922cadeb97140274cba", + "wall_seconds": 3.180392162874341 + }, + "r2p1_ref_vs_0.5": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 1.4772143877471049e-05, + "max_logg_difference": 2.098608620215714e-09 + }, + "r2p1_ref_vs_2": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 1.4772101910544689e-05, + "max_logg_difference": 2.0455042104572385e-09 + }, + "r2p1_ref_vs_8": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 1.4772018211627154e-05, + "max_logg_difference": 2.059625359152051e-09 + }, + "r2p1_ref_vs_32": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 1.4772020763985666e-05, + "max_logg_difference": 2.068350379857975e-09 + } +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_summary.json b/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_summary.json new file mode 100644 index 0000000..9fbbb41 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/mesh_summary.json @@ -0,0 +1,354 @@ +{ + "r100_hmax0.5": { + "vertices": 3158, + "triangles": 6188, + "outcomes": { + "0": 2906, + "1": 252 + }, + "reasons": { + "0": 2906, + "1": 252 + }, + "accepted": 2269662, + "rejected": 177, + "rhs": 16813349, + "accepted_percentiles": { + "0": 312, + "0.5": 736, + "0.9": 786, + "0.99": 848, + "1": 899 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 0, + "0.9": 0, + "0.99": 1, + "1": 1 + }, + "rhs_percentiles": { + "0": 2471, + "0.5": 5446, + "0.9": 5803, + "0.99": 6223, + "1": 6587 + }, + "sha256": "787386a026203d846debf25a6a4f431a131b06bd3ab464f75f3e17a1d379992a", + "wall_seconds": 2.0714269301388413 + }, + "r100_hmax2": { + "vertices": 3158, + "triangles": 6188, + "outcomes": { + "0": 2906, + "1": 252 + }, + "reasons": { + "0": 2906, + "1": 252 + }, + "accepted": 723677, + "rejected": 5690, + "rhs": 6030185, + "accepted_percentiles": { + "0": 165, + "0.5": 220, + "0.9": 290, + "0.99": 348, + "1": 397 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 2, + "0.9": 4, + "0.99": 5, + "1": 5 + }, + "rhs_percentiles": { + "0": 1442, + "0.5": 1855, + "0.9": 2324, + "0.99": 2737, + "1": 3073 + }, + "sha256": "187c3cbba6a18d88608cd3859da94d88b58ce9d65e0d800d84b3254a6b139151", + "wall_seconds": 0.9786226029973477 + }, + "r100_hmax8": { + "vertices": 3158, + "triangles": 6188, + "outcomes": { + "0": 2906, + "1": 252 + }, + "reasons": { + "0": 2906, + "1": 252 + }, + "accepted": 463004, + "rejected": 9336, + "rhs": 4230835, + "accepted_percentiles": { + "0": 80, + "0.5": 133, + "0.9": 225, + "0.99": 286, + "1": 357 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 3, + "0.9": 5, + "0.99": 6, + "1": 7 + }, + "rhs_percentiles": { + "0": 868, + "0.5": 1253, + "0.9": 1869, + "0.99": 2289, + "1": 2786 + }, + "sha256": "4c7e9d401f2af237129a6a677ca94ed1ee41472594db57852a08647b1dc34aad", + "wall_seconds": 0.8228706018999219 + }, + "r100_hmax32": { + "vertices": 3158, + "triangles": 6188, + "outcomes": { + "0": 2906, + "1": 252 + }, + "reasons": { + "0": 2906, + "1": 252 + }, + "accepted": 441750, + "rejected": 9336, + "rhs": 4081609, + "accepted_percentiles": { + "0": 72, + "0.5": 126, + "0.9": 222, + "0.99": 285, + "1": 357 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 3, + "0.9": 5, + "0.99": 6, + "1": 7 + }, + "rhs_percentiles": { + "0": 812, + "0.5": 1204, + "0.9": 1848, + "0.99": 2282, + "1": 2786 + }, + "sha256": "3742efd56893bec9e62a01668915ef7dda613d6504b2057043eb97c9b38334f3", + "wall_seconds": 0.8300854018889368 + }, + "r100_2_vs_0.5": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 3.077090601306577e-08, + "max_logg_difference": 5.28674309191457e-11 + }, + "r100_2_vs_8": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 7.063214322895989e-09, + "max_logg_difference": 3.2702011237839557e-11 + }, + "r100_2_vs_32": { + "shared": 3158, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 7.063404202987903e-09, + "max_logg_difference": 2.477072019724247e-11 + }, + "r2p1_hmax0.5": { + "vertices": 5496, + "triangles": 10816, + "outcomes": { + "1": 1336, + "0": 4160 + }, + "reasons": { + "1": 1336, + "0": 4160 + }, + "accepted": 2646842, + "rejected": 830, + "rhs": 20136508, + "accepted_percentiles": { + "0": 116, + "0.5": 552, + "0.9": 618, + "0.99": 672, + "1": 823 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 0, + "0.9": 1, + "0.99": 1, + "1": 2 + }, + "rhs_percentiles": { + "0": 1099, + "0.5": 4158, + "0.9": 4620, + "0.99": 4991, + "1": 6055 + }, + "sha256": "3a77a1e7dee3cd18d3ab8a408826aba932f912dc88749989e0c307013d6e7e81", + "wall_seconds": 1.973590083885938 + }, + "r2p1_hmax2": { + "vertices": 5496, + "triangles": 10816, + "outcomes": { + "1": 1336, + "0": 4160 + }, + "reasons": { + "1": 1336, + "0": 4160 + }, + "accepted": 1131104, + "rejected": 4004, + "rhs": 9548434, + "accepted_percentiles": { + "0": 109, + "0.5": 202, + "0.9": 258, + "0.99": 311, + "1": 460 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 1, + "0.9": 1, + "0.99": 2, + "1": 2 + }, + "rhs_percentiles": { + "0": 1050, + "0.5": 1708, + "0.9": 2100, + "0.99": 2478, + "1": 3521 + }, + "sha256": "899331ea41d1612cf66bc7664b50e866f4f3bc7309e9c206440f801238ca455c", + "wall_seconds": 1.177934248931706 + }, + "r2p1_hmax8": { + "vertices": 5496, + "triangles": 10816, + "outcomes": { + "1": 1336, + "0": 4160 + }, + "reasons": { + "1": 1336, + "0": 4160 + }, + "accepted": 822680, + "rejected": 4004, + "rhs": 7389760, + "accepted_percentiles": { + "0": 87, + "0.5": 137, + "0.9": 212, + "0.99": 265, + "1": 386 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 1, + "0.9": 1, + "0.99": 2, + "1": 2 + }, + "rhs_percentiles": { + "0": 910, + "0.5": 1260, + "0.9": 1778, + "0.99": 2142, + "1": 3003 + }, + "sha256": "49edfbe9dfb2948d3472e6ef6287bed103332758953756a7e4089316caa0b1b6", + "wall_seconds": 0.9704615350347012 + }, + "r2p1_hmax32": { + "vertices": 5496, + "triangles": 10816, + "outcomes": { + "1": 1336, + "0": 4160 + }, + "reasons": { + "1": 1336, + "0": 4160 + }, + "accepted": 795354, + "rejected": 4004, + "rhs": 7197806, + "accepted_percentiles": { + "0": 81, + "0.5": 132, + "0.9": 210, + "0.99": 261, + "1": 379 + }, + "rejected_percentiles": { + "0": 0, + "0.5": 1, + "0.9": 1, + "0.99": 2, + "1": 2 + }, + "rhs_percentiles": { + "0": 861, + "0.5": 1218, + "0.9": 1757, + "0.99": 2128, + "1": 2954 + }, + "sha256": "74bbb1e969644d49b7fdf5a6a7a5930ca8b962ec91628cd91feeb9df039100e7", + "wall_seconds": 0.9720155198592693 + }, + "r2p1_2_vs_0.5": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 1.046601929593161e-10, + "max_logg_difference": 6.900746640781108e-11 + }, + "r2p1_2_vs_8": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 8.369894549651799e-11, + "max_logg_difference": 1.9082291302652266e-11 + }, + "r2p1_2_vs_32": { + "shared": 5496, + "only_a": 0, + "only_b": 0, + "terminal_mismatches": 0, + "max_sky_angle": 8.114658658759588e-11, + "max_logg_difference": 2.823297151621773e-11 + } +} diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/source_environment.txt b/benchmarks/adaptive_step_bounds_2026-10-05/results/source_environment.txt new file mode 100644 index 0000000..27dec94 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/source_environment.txt @@ -0,0 +1,54 @@ +# Experiment A/B environment and source identity +date_utc: 2026-10-05T22:15:50Z +host: Linux ATRI-gentoo 6.18.48-gentoo #1 SMP PREEMPT_DYNAMIC Mon Aug 31 13:33:24 EDT 2026 x86_64 12th Gen Intel(R) Core(TM) i7-12700K GenuineIntel GNU/Linux +nproc: 16 +cc: +cc (Gentoo 16.2.1_p20260926 p1) 16.2.1 20260926 +Copyright (C) 2026 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + +git_head: c894fdb11a0845558fdb67b71a9bd019a2d94ed4 +git_status_short: + M Makefile + M mk/reference_images.mk + M nr_spacetime_movie_renderer_design.md + M src/asymptotic.c + M src/frame.c + M src/frame.h + M src/geodesic.c + M src/geodesic.h + M src/lens_map.c + M src/lens_map.h + M src/main.c + M src/ray.c + M src/ray.h + M tests/test_asymptotic.c + M tests/test_camera_cli.py + M tests/test_frame.c + M usage.md +?? nmesh_spacetime_output_format.md +?? tests/test_adaptive_cli.py +?? tests/test_geodesic_adaptive.c + +# sha256 of the sources consumed by the harnesses +25f24a2e1c9ce49508fed230dfb3f6d9986e92bb4a5e6347bb2356ac0ee5fffa src/geodesic.c +1e43b95346f484050354631e8682f9793e618cbbd1de6338bd15e092216ae6fb src/geodesic.h +e6c0616461507adb1141ca3caa7e76f30e2497c4d53acfef092597fb60431a4a src/asymptotic.c +db029ef837f12800bbf81fc74e7bab6eb9f18e42a53ceb5f205867d8a329321d src/asymptotic.h +1792a17f432ceee32ee9c802486ecf9d64d96307c249e0d5cfa563ef4a0fefd5 src/asymptotic_schwarzschild.c +cc9ad0b04e11bd08bcf238901125e76a26ec332cd7fe72871e7c46fb7a322aec src/asymptotic_schwarzschild.h +ea0b5838bc56fbfeedec9110cd20653b892127bfc863db91a3a8cabaad4c520b src/asymptotic_gl48.h +359066d78a10f9589a220124147cdbefaff8f97028a9fdd2923b474349599752 src/spacetime.h +4c7ae6e96fe57e2a2ae2491db924bad56af59abfcc305bbb6ab2609f3b405e0d src/spacetime_common.c +690045ed715a951335dc7d654258e0f3b802e0bf6e2815ceb4858fa99cd372d7 src/spacetime_minkowski.c +e5b5935d16e24b07cedb25d184e8f7029075aec9496f54c23cb1061971914a72 src/spacetime_schwarzschild.c +75956034303fe3bab9324ecb87533e054458e35a26132a3e885233acad785767 src/spacetime_alcubierre.c +559f626983c68e06d3223d8381085a0b5ac422529a1ea656f4e24b1b3a50f51a src/observer.c +d761aabfbc277b0989bd20302578302d91f8253a9d900943de62ab3edd3f145b src/observer.h + +# sha256 of the harnesses and this script +853018775c335180199aa3a0046445a5f795f83d18de0ff6a664a6368d835dbd a_public_endpoints.c +10a6558815e3ff5fa114aa8ca6dfe3c7db6e860ecb9f7cab20c59145a662d3d2 b_actual_h.c +1cfd93b0c9f187b4c45d169fbeb8c6fea7f02c3317ac24a32691723b0086548c run_experiment.sh diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/results/summary_tables.txt b/benchmarks/adaptive_step_bounds_2026-10-05/results/summary_tables.txt new file mode 100644 index 0000000..11e2744 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/results/summary_tables.txt @@ -0,0 +1,318 @@ +# Experiment A/B summary tables + +## A Schwarzschild references (DP tol=1e-12, max_step=0.25) + +| case | dir | theta | outcome | reason | stop_t | steps | rej | rhs | g | thr | +|---|---|---|---|---|---|---|---|---|---|---| +| r30_inward | 0 | 0 | DARK | REDSHIFT | -69.57 | 531 | 0 | 4004 | 0 | 8 | +| r30_inward | 1 | 3.142 | ESC | NONE | -226 | 905 | 0 | 6629 | 1.035 | nan | +| r30_inward | 2 | 1.571 | ESC | NONE | -256.7 | 1028 | 0 | 7490 | 1.035 | nan | +| r30_inward | 3 | 0.1681 | DARK | REDSHIFT | -225.9 | 3746 | 0 | 26509 | 0 | 8 | +| r30_inward | 4 | 0.1671 | DARK | REDSHIFT | -94.28 | 1127 | 0 | 8176 | 0 | 8 | +| r30_inward | 5 | 0.1691 | ESC | NONE | -322.4 | 1834 | 0 | 13132 | 1.035 | nan | +| r30_inward | 6 | 0.1681 | DARK | REDSHIFT | -118.2 | 1606 | 0 | 11529 | 0 | 8 | +| r30_inward | 7 | 0.1681 | ESC | NONE | -346.3 | 2312 | 1 | 16485 | 1.035 | nan | +| r30_inward | 8 | 0.1681 | DARK | REDSHIFT | -142.2 | 2081 | 0 | 14854 | 0 | 8 | +| r30_inward | 9 | 0.1681 | ESC | NONE | -370.2 | 2790 | 1 | 19831 | 1.035 | nan | +| r30_inward | 10 | 0.2181 | ESC | NONE | -302.6 | 1409 | 1 | 10164 | 1.035 | nan | +| r30_inward | 11 | 0.1181 | DARK | REDSHIFT | -73.76 | 665 | 0 | 4942 | 0 | 8 | +| r100_inward | 0 | 0 | DARK | REDSHIFT | -144.3 | 830 | 0 | 6097 | 0 | 8 | +| r100_inward | 1 | 3.142 | ESC | NONE | -156 | 625 | 0 | 4669 | 1.01 | nan | +| r100_inward | 2 | 1.571 | ESC | NONE | -237.7 | 952 | 0 | 6958 | 1.01 | nan | +| r100_inward | 3 | 0.05146 | DARK | REDSHIFT | -301.1 | 4045 | 0 | 28602 | 0 | 8 | +| r100_inward | 4 | 0.05046 | DARK | REDSHIFT | -163.1 | 1292 | 0 | 9331 | 0 | 8 | +| r100_inward | 5 | 0.05246 | ESC | NONE | -391.6 | 2014 | 1 | 14399 | 1.01 | nan | +| r100_inward | 6 | 0.05145 | DARK | REDSHIFT | -187.1 | 1773 | 0 | 12698 | 0 | 8 | +| r100_inward | 7 | 0.05147 | ESC | NONE | -415.4 | 2493 | 0 | 17745 | 1.01 | nan | +| r100_inward | 8 | 0.05146 | DARK | REDSHIFT | -211 | 2252 | 0 | 16051 | 0 | 8 | +| r100_inward | 9 | 0.05146 | ESC | NONE | -439.3 | 2969 | 0 | 21077 | 1.01 | nan | +| r100_inward | 10 | 0.1015 | ESC | NONE | -372.3 | 1571 | 0 | 11291 | 1.01 | nan | +| r100_inward | 11 | 0.001462 | DARK | REDSHIFT | -144.3 | 830 | 0 | 6097 | 0 | 8 | +| r2p1_outward | 0 | 0 | ESC | NONE | -253.9 | 1120 | 1 | 8141 | 4.583 | nan | +| r2p1_outward | 1 | 3.142 | DARK | REDSHIFT | -32.25 | 186 | 0 | 1589 | 0 | 8 | +| r2p1_outward | 2 | 1.571 | DARK | REDSHIFT | -32.77 | 316 | 1 | 2506 | 0 | 8 | +| r2p1_outward | 3 | 0.5704 | ESC | NONE | -414.9 | 4263 | 2 | 30149 | 4.583 | nan | +| r2p1_outward | 4 | 0.5694 | ESC | NONE | -285.6 | 1686 | 1 | 12103 | 4.583 | nan | +| r2p1_outward | 5 | 0.5714 | DARK | REDSHIFT | -61.58 | 995 | 1 | 7259 | 0 | 8 | +| r2p1_outward | 6 | 0.5704 | ESC | NONE | -309.5 | 2162 | 2 | 15442 | 4.583 | nan | +| r2p1_outward | 7 | 0.5704 | DARK | REDSHIFT | -85.49 | 1474 | 1 | 10612 | 0 | 8 | +| r2p1_outward | 8 | 0.5704 | ESC | NONE | -333.5 | 2639 | 1 | 18774 | 4.583 | nan | +| r2p1_outward | 9 | 0.5704 | DARK | REDSHIFT | -109.4 | 1950 | 1 | 13944 | 0 | 8 | +| r2p1_outward | 10 | 0.6204 | DARK | REDSHIFT | -41.99 | 585 | 1 | 4389 | 0 | 8 | +| r2p1_outward | 11 | 0.5204 | ESC | NONE | -265 | 1303 | 2 | 9429 | 4.583 | nan | +| r1p5_freefall | 0 | 0 | DARK | REDSHIFT | -30.66 | 231 | 1 | 1911 | 0 | 8 | +| r1p5_freefall | 1 | 1.571 | ESC | NONE | -255.2 | 1130 | 1 | 8211 | 1 | nan | +| r1p5_freefall | 2 | 3.142 | ESC | NONE | -254.5 | 1143 | 1 | 8302 | 0.4641 | nan | +| r1p5_freefall | 3 | 2.356 | ESC | NONE | -254.6 | 1127 | 1 | 8190 | 0.5505 | nan | + +## A Schwarzschild upper scan (min_step=1e-12, tol=1e-9) + +| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| r100_inward | 0.25 | 0 | 2.604e-05 | 2.048e-09 | 35.2 | 93464 | 0 | 6 | 6 | 0 | 0 | +| r100_inward | 0.5 | 0 | 5.696e-05 | 2.056e-09 | 39.27 | 56595 | 0 | 6 | 6 | 0 | 0 | +| r100_inward | 1 | 0 | 8.348e-05 | 2.051e-09 | 41.25 | 39788 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 2 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 4 | 0 | 8.089e-05 | 2.028e-09 | 41.09 | 29393 | 2 | 6 | 6 | 0 | 0 | +| r100_inward | 8 | 0 | 8.034e-05 | 2.034e-09 | 41.06 | 28427 | 3 | 6 | 6 | 0 | 0 | +| r100_inward | 16 | 0 | 8.034e-05 | 2.042e-09 | 41.06 | 28189 | 3 | 6 | 6 | 0 | 0 | +| r100_inward | 32 | 0 | 8.034e-05 | 2.043e-09 | 41.06 | 28182 | 3 | 6 | 6 | 0 | 0 | +| r100_inward | 64 | 0 | 8.034e-05 | 2.043e-09 | 41.06 | 28182 | 3 | 6 | 6 | 0 | 0 | +| r1p5_freefall | 0.25 | 0 | 1.002e-10 | 3.165e-10 | 2.608e-08 | 23597 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 0.5 | 0 | 8.875e-11 | 3.658e-10 | 3.089e-08 | 12747 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1 | 0 | 8.187e-11 | 3.811e-10 | 2.817e-08 | 7553 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 2 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 4 | 0 | 8.157e-11 | 3.632e-10 | 2.346e-08 | 4039 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 8 | 0 | 8.151e-11 | 3.547e-10 | 2.125e-08 | 3591 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 16 | 0 | 8.146e-11 | 3.469e-10 | 1.925e-08 | 3465 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 32 | 0 | 8.146e-11 | 3.46e-10 | 1.948e-08 | 3458 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 64 | 0 | 8.146e-11 | 3.46e-10 | 1.948e-08 | 3458 | 0 | 3 | 1 | 0 | 0 | +| r2p1_outward | 0.25 | 1 | 0.0002872 | 2.661e-09 | 263.1 | 62258 | 0 | 5 | 7 | 0 | 0 | +| r2p1_outward | 0.5 | 1 | 0.0002872 | 2.644e-09 | 263.1 | 42490 | 0 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 33544 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 2 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 4 | 1 | 0.0002872 | 2.598e-09 | 263.1 | 27580 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 8 | 1 | 0.0002872 | 2.606e-09 | 263.1 | 26831 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 16 | 1 | 0.0002872 | 2.614e-09 | 263.1 | 26614 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 32 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 26607 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 64 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 26607 | 1 | 5 | 7 | 0 | 0 | +| r30_inward | 0.25 | 0 | 8.667e-05 | 2.262e-09 | 35.08 | 76510 | 0 | 6 | 6 | 0 | 0 | +| r30_inward | 0.5 | 0 | 0.0001866 | 2.268e-09 | 39.06 | 49063 | 1 | 6 | 6 | 0 | 0 | +| r30_inward | 1 | 0 | 0.0002801 | 2.266e-09 | 41.17 | 36974 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 2 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 4 | 0 | 0.0002819 | 2.25e-09 | 41.2 | 29617 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 8 | 0 | 0.0002819 | 2.258e-09 | 41.2 | 28714 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 16 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28455 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 32 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28434 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 64 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28434 | 2 | 6 | 6 | 0 | 0 | + +## A Schwarzschild min scan (max_step=2, tol=1e-9) + +| case | min_step | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| r100_inward | 1e-14 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 1e-12 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 1e-10 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 1e-08 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 1e-06 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 1e-05 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 0.0001 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 0.001 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 0.01 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r100_inward | 0.1 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 | +| r1p5_freefall | 1e-14 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1e-12 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1e-10 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1e-08 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1e-06 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 1e-05 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 0.0001 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 0.001 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 0.01 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 | +| r1p5_freefall | 0.1 | 1 | 8.165e-11 | 3.719e-10 | 254.3 | 3654 | 0 | 2 | 1 | 0 | 1 | +| r2p1_outward | 1e-14 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1e-12 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1e-10 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1e-08 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1e-06 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 1e-05 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 0.0001 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 0.001 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 0.01 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r2p1_outward | 0.1 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 | +| r30_inward | 1e-14 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 1e-12 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 1e-10 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 1e-08 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 1e-06 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 1e-05 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 0.0001 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 0.001 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 0.01 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | +| r30_inward | 0.1 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 | + +## A Schwarzschild sensitive RK4 .01 vs .005 + +| case | dir | theta | out.01 | out.005 | hhalve_dn_ang | |dg| | rhs.01 | rhs.005 | +|---|---|---|---|---|---|---|---|---| +| r2p1_outward | 6 | 0.5704 | ESC | ESC | 1.032e-07 | 7.386e-12 | 124072 | 247880 | +| r2p1_outward | 7 | 0.5704 | DARK | DARK | nan | 0 | 34196 | 68392 | +| r2p1_outward | 8 | 0.5704 | ESC | ESC | 1.04e-05 | 7.618e-12 | 133644 | 267024 | +| r2p1_outward | 9 | 0.5704 | DARK | DARK | nan | 0 | 43768 | 87536 | +| r30_inward | 6 | 0.1681 | DARK | DARK | nan | 0 | 47300 | 94600 | +| r30_inward | 7 | 0.1681 | ESC | ESC | 0 | 8.238e-14 | 138764 | 277264 | +| r30_inward | 8 | 0.1681 | DARK | DARK | nan | 0 | 56872 | 113744 | +| r30_inward | 9 | 0.1681 | ESC | ESC | 1.891e-07 | 7.55e-15 | 148336 | 296408 | + +## A Minkowski flat analytic check (n_inf and t) + +| upper | min_step | n | max_dn_ang | max_dgrel | max_t_err | +|---|---|---|---|---|---| +| 1 | 1e-12 | 6 | 0 | 0 | 5.258e-13 | +| 1 | 1e-06 | 6 | 0 | 0 | 5.258e-13 | +| 1 | 0.1 | 6 | 0 | 0 | 5.258e-13 | +| 4 | 1e-12 | 6 | 0 | 0 | 1.776e-12 | +| 4 | 1e-06 | 6 | 0 | 0 | 1.776e-12 | +| 4 | 0.1 | 6 | 0 | 0 | 1.776e-12 | +| 16 | 1e-12 | 6 | 0 | 0 | 1.3e-11 | +| 16 | 1e-06 | 6 | 0 | 0 | 1.3e-11 | +| 16 | 0.1 | 6 | 0 | 0 | 1.3e-11 | +| 64 | 1e-12 | 6 | 0 | 0 | 3.894e-11 | +| 64 | 1e-06 | 6 | 0 | 0 | 3.894e-11 | +| 64 | 0.1 | 6 | 0 | 0 | 3.894e-11 | +| 256 | 1e-12 | 6 | 0 | 0 | 7.661e-11 | +| 256 | 1e-06 | 6 | 0 | 0 | 7.661e-11 | +| 256 | 0.1 | 6 | 0 | 0 | 7.661e-11 | + +## A Minkowski flat analytic check (crossing x and t) + +| upper | min_step | n | max_x_err | max_t_err | +|---|---|---|---|---| +| 1 | 1e-12 | 6 | 4.832e-13 | 5.258e-13 | +| 1 | 1e-06 | 6 | 4.832e-13 | 5.258e-13 | +| 1 | 0.1 | 6 | 4.832e-13 | 5.258e-13 | +| 4 | 1e-12 | 6 | 1.72e-12 | 1.776e-12 | +| 4 | 1e-06 | 6 | 1.72e-12 | 1.776e-12 | +| 4 | 0.1 | 6 | 1.72e-12 | 1.776e-12 | +| 16 | 1e-12 | 6 | 1.268e-11 | 1.3e-11 | +| 16 | 1e-06 | 6 | 1.268e-11 | 1.3e-11 | +| 16 | 0.1 | 6 | 1.268e-11 | 1.3e-11 | +| 64 | 1e-12 | 6 | 3.506e-11 | 3.894e-11 | +| 64 | 1e-06 | 6 | 3.506e-11 | 3.894e-11 | +| 64 | 0.1 | 6 | 3.506e-11 | 3.894e-11 | +| 256 | 1e-12 | 6 | 7.061e-11 | 7.661e-11 | +| 256 | 1e-06 | 6 | 7.061e-11 | 7.661e-11 | +| 256 | 0.1 | 6 | 7.061e-11 | 7.661e-11 | + +## A Alcubierre alc_v3_s1 upper scan + +| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 | +| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 | + +## A Alcubierre alc_v3_s10 upper scan + +| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 | +| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 | + +## A Alcubierre alc_v9_s1 upper scan + +| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 | +| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 | + +## A Alcubierre alc_v9_s10 upper scan + +| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 | +| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 | + +## A Alcubierre v9 s100 lower-bound stress (upper=0.004) + +| case | min_step | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC | +|---|---|---|---|---|---|---|---|---|---|---|---| +| alc_v9_s100 | 1e-14 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s100 | 1e-12 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s100 | 1e-08 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s100 | 1e-06 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s100 | 1e-05 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | +| alc_v9_s100 | 0.0001 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 | + +## B observed accepted step h (finite T=min(20, ref span)) + +| case | dir | ref | span | target | steps | reached | term | h_min | h_max | h_first | bnd | rhs | rej | null_max | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| r30_inward | 0 | DARK | 69.57 | -20 | 17 | 1 | -1 | 0.1 | 1.726 | 0.1 | 1 | 119 | 0 | 3.009e-12 | +| r30_inward | 1 | ESC | 226 | -20 | 13 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 91 | 0 | 8.69e-12 | +| r30_inward | 2 | DARK | 142.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 | +| r30_inward | 3 | ESC | 370.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 | +| r30_inward | 4 | DARK | 118.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 | +| r30_inward | 5 | ESC | 346.3 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 | +| r100_inward | 0 | DARK | 144.3 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.454e-14 | +| r100_inward | 1 | ESC | 156 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 7.661e-15 | +| r100_inward | 2 | DARK | 301.1 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.399e-14 | +| r100_inward | 3 | DARK | 211 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.321e-14 | +| r2p1_outward | 0 | ESC | 253.9 | -20 | 46 | 1 | -1 | 0.1 | 1.071 | 0.1 | 1 | 322 | 0 | 6.274e-11 | +| r2p1_outward | 1 | ESC | 414.9 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 | +| r2p1_outward | 2 | ESC | 333.5 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 | +| r2p1_outward | 3 | DARK | 109.4 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 | +| r2p1_outward | 4 | DARK | 32.25 | -20 | 37 | 1 | -1 | 0.1 | 0.8994 | 0.1 | 1 | 259 | 0 | 2.991e-11 | +| r1p5_freefall | 0 | DARK | 30.66 | -20 | 49 | 1 | -1 | 0.1 | 0.696 | 0.1 | 1 | 343 | 0 | 4.938e-10 | +| r1p5_freefall | 1 | ESC | 254.5 | -20 | 52 | 1 | -1 | 0.0921 | 1.084 | 0.1 | 1 | 364 | 0 | 4.034e-11 | +| mink_moving | 0 | ESC | 51.26 | -20 | 3 | 1 | -1 | 1 | 14 | 1 | 1 | 21 | 0 | 2.22e-16 | +| mink_moving | 1 | ESC | 71.58 | -20 | 3 | 1 | -1 | 1 | 14 | 1 | 1 | 21 | 0 | 0 | +| alc_v3_s1 | 0 | ESC | 16.43 | -16.43 | 56 | 1 | -1 | 0.05 | 0.4 | 0.05 | 1 | 406 | 2 | 0 | +| alc_v3_s1 | 1 | ESC | 29.38 | -20 | 62 | 1 | -1 | 0.05 | 0.4 | 0.05 | 1 | 441 | 1 | 0 | +| alc_v9_s10 | 0 | ESC | 2.037 | -2.037 | 77 | 1 | -1 | 0.005 | 0.04 | 0.005 | 1 | 588 | 7 | 0 | +| alc_v9_s10 | 1 | ESC | 19.96 | -19.96 | 521 | 1 | -1 | 0.005 | 0.04 | 0.005 | 1 | 3682 | 5 | 2.22e-16 | +| alc_v9_s100 | 0 | ESC | 1.104 | -1.104 | 302 | 1 | -1 | 0.0005 | 0.004 | 0.0005 | 1 | 2163 | 7 | 0 | +| alc_v9_s100 | 1 | ESC | 2.896 | -2.896 | 746 | 1 | -1 | 0.0005 | 0.004 | 0.0005 | 1 | 5257 | 5 | 2.22e-16 | diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_4k_pair.py b/benchmarks/adaptive_step_bounds_2026-10-05/run_4k_pair.py new file mode 100755 index 0000000..7377522 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_4k_pair.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""One-task, at-most-two-attempt 4K comparison (R100, 3840x2160, FOV45). + +This driver does NOT run as part of routine verification. It refuses to do +anything unless ``--authorize-two-4k`` is passed explicitly, and an attempt +ledger makes the task non-repeatable. Any future execution requires a fresh, +explicit user authorization for that task; authorization never extends to later +tasks or routine tests. + +Usage: + run_4k_pair.py --authorize-two-4k [--out DIR] + +OUT defaults to /local/adaptive_bounds_4k (distinct from any older +one-off ledger). The renderer binary is expected at +build/Release/schwarzschild_sky (see README for the make command). +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import time + +from mesh_maps import load, compare + +ROOT = Path(__file__).resolve().parents[2] +ENV = dict(os.environ, OMP_NUM_THREADS='16', OMP_DYNAMIC='FALSE') +BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend' + +REFUSAL = ( + 'REFUSING TO RUN: the 4K hmax 2-vs-8 pair is a one-task, at-most-two-attempt ' + 'experiment. Pass --authorize-two-4k only after an explicit fresh user ' + 'authorization for this task. It is not a routine verification target.') + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--authorize-two-4k', action='store_true', + help='explicit authorization for the two-attempt 4K pair') + ap.add_argument('--out', default=str(ROOT / 'local/adaptive_bounds_4k'), + help='output directory (default: local/adaptive_bounds_4k)') + args = ap.parse_args() + if not args.authorize_two_4k: + print(REFUSAL, file=sys.stderr) + return 2 + out = Path(args.out).resolve() + binary = ROOT / 'build/Release/schwarzschild_sky' + if not binary.exists(): + print(f'missing {binary}; build it first:\n {BUILD_CMD}', + file=sys.stderr) + return 2 + out.mkdir(parents=True, exist_ok=True) + ledger = out / 'attempts.json' + attempts = json.loads(ledger.read_text()) if ledger.exists() else [] + if attempts: + print('This one-task pair has already been attempted; no reruns ' + 'authorized.', file=sys.stderr) + return 3 + catalog = out / 'single_dim_star.csv' + catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n' + '262.5,-30,6000,1e-30\n') + with (out / 'cpu.txt').open('w') as f: + subprocess.run(['lscpu'], stdout=f, check=True) + metadata = { + 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), + 'utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), + 'source_sha256': { + p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() + for p in ('src/main.c', 'src/geodesic.c', 'src/frame.c')}, + 'catalog': 'one negligible synthetic point; no survey catalog', + 'scope': 'README R100 45-degree 3840x2160 coarse16 refine4 jacobian0.2', + 'threads': 16, 'max_attempts': 2, + 'authorization': 'explicit --authorize-two-4k for this task only'} + (out / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n') + maps = {} + results = {} + for upper in (2, 8): + name = f'r100_4k_hmax{upper}' + cmd = [str(binary), '--integrator', 'dp54', '--width', '3840', + '--height', '2160', '--look-ra-deg', '262.5', + '--look-dec-deg', '-30', '--fov-deg', '45', + '--observer-radius', '100', '--coarse-cell-pixels', '16', + '--refine-max-level', '4', '--refine-jacobian-min', '.2', + '--catalog', str(catalog), '--exposure', '1', + '--tone-map', 'reinhard', '--psf-min-y', '1e-20', + '--psf-relative-tail', '1e-4', '--ode-min-step', '1e-12', + '--ode-max-step', str(upper), '--verbose', + '--lens-map-output', str(out / (name + '.grlens')), + '--output', str(out / (name + '.png'))] + attempts.append({'upper': upper, 'command': shlex.join(cmd), + 'state': 'attempted'}) + ledger.write_text(json.dumps(attempts, indent=2) + '\n') + start = time.perf_counter() + with (out / (name + '.log')).open('w') as log: + log.write('OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE ' + + shlex.join(cmd) + '\n') + log.flush() + result = subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log, + stderr=subprocess.STDOUT, timeout=900) + attempts[-1].update(returncode=result.returncode, + wall_seconds=time.perf_counter() - start, + state='completed') + ledger.write_text(json.dumps(attempts, indent=2) + '\n') + if result.returncode: + raise SystemExit(f'4K attempt failed: {name}; no automatic retry.') + info, maps[upper] = load(out / (name + '.grlens')) + info['wall_seconds'] = attempts[-1]['wall_seconds'] + results[name] = info + print(name, json.dumps(info), flush=True) + results['comparison'] = compare(maps[2], maps[8]) + p, q = (results[f'r100_4k_hmax{h}']['provenance'] for h in (2, 8)) + assert p['max_step'] == 2 and q['max_step'] == 8 + assert {k: v for k, v in p.items() if k != 'max_step'} == \ + {k: v for k, v in q.items() if k != 'max_step'} + print('comparison', json.dumps(results['comparison']), flush=True) + (out / 'summary.json').write_text(json.dumps(results, indent=2) + '\n') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh b/benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh new file mode 100755 index 0000000..4d24b43 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Run the standalone critical-classification reference check +# (benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c). +# +# benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh [OUT_DIR] +# +# OUT_DIR defaults to /local/adaptive_step_bounds_2026-10-05. All output +# stays under OUT_DIR; the tracked benchmark directory holds only the sources. +# This is a diagnostic probe (see the C file header); it makes no pass/fail +# claim about the exactly-critical physical outcome. +set -euo pipefail + +SOURCE=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$SOURCE/../.." && pwd) +OUT=${1:-$root/local/adaptive_step_bounds_2026-10-05} +BIN=/tmp/opencode/step_bounds + +mkdir -p "$OUT/raw" "$OUT/logs" "$BIN" +cd "$OUT" +OUT=$PWD + +{ + echo "### building critical_ref_check" + set -x + cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \ + "$SOURCE/critical_ref_check.c" "$root/src/geodesic.c" \ + "$root/src/asymptotic.c" "$root/src/asymptotic_schwarzschild.c" \ + "$root/src/spacetime_common.c" "$root/src/observer.c" -lm \ + -o "$BIN/critical_ref_check" + echo "### command: critical_ref_check" + time "$BIN/critical_ref_check" + set +x +} > logs/critical_ref.log 2>&1 + +cat logs/critical_ref.log +echo "outputs under $OUT" diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh b/benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh new file mode 100755 index 0000000..7a1fb8f --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Build and run the failure-path floor diagnostic. +# +# benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh [OUT_DIR] +# +# OUT_DIR defaults to /local/adaptive_bounds_mesh. All output stays +# under OUT_DIR; the tracked benchmark directory holds only the sources. +# +# failure_floor.c includes ../../tests/test_geodesic_adaptive.c (depth-2 path +# valid from this directory) and therefore requires -DGEODESIC_EVENT_TESTING +# and the production RayPool source src/ray.c. The two analytic backends are +# textually included by the regression file, so they are not linked here. +set -euo pipefail + +SOURCE=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$SOURCE/../.." && pwd) +OUT=${1:-$root/local/adaptive_bounds_mesh} +BIN=/tmp/opencode/step_bounds + +mkdir -p "$OUT/logs" "$BIN" +cd "$OUT" + +{ + set -x + cc -std=c11 -O2 -Wall -Wextra -Wpedantic -fopenmp -DGEODESIC_EVENT_TESTING \ + -I"$root/src" \ + "$SOURCE/failure_floor.c" "$root/src/geodesic.c" "$root/src/ray.c" \ + "$root/src/spacetime_common.c" "$root/src/observer.c" \ + "$root/src/asymptotic.c" "$root/src/asymptotic_schwarzschild.c" -lm \ + -o "$BIN/failure_floor" + "$BIN/failure_floor" + set +x +} > logs/failure_floor.log 2>&1 + +cat logs/failure_floor.log +echo "outputs under $OUT" diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh b/benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh new file mode 100755 index 0000000..0125f89 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Long-term reproduction driver for the adaptive step-bound scan (Experiment A/B). +# +# benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh [OUT_DIR] +# +# OUT_DIR defaults to /local/adaptive_step_bounds_2026-10-05. All build +# and run output — env/hash record, build log, stdout logs and per-ray CSV — +# is written under OUT_DIR, never into this tracked benchmark directory. +# Scratch binaries go to /tmp/opencode/step_bounds. +set -euo pipefail + +SOURCE=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$SOURCE/../.." && pwd) +OUT=${1:-$root/local/adaptive_step_bounds_2026-10-05} +BIN=/tmp/opencode/step_bounds + +mkdir -p "$OUT/raw" "$OUT/logs" "$BIN" +cd "$OUT" +OUT=$PWD + +SRC_COMMON="$root/src/geodesic.c $root/src/asymptotic.c \ +$root/src/asymptotic_schwarzschild.c $root/src/spacetime_common.c \ +$root/src/observer.c" + +# --- environment + source identity (full shell output) -------------------- +{ + echo "# adaptive_step_bounds_2026-10-05 environment and source identity" + echo "date_utc: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "host: $(uname -a)" + echo "nproc: $(nproc)" + echo "cc:"; cc --version + echo + echo "git_head: $(git -C "$root" rev-parse HEAD 2>/dev/null || echo none)" + echo "git_status_short:"; git -C "$root" status --short 2>/dev/null || true + echo + echo "# sha256 of the production sources consumed by the harnesses" + sha256sum $SRC_COMMON + echo + echo "# sha256 of the benchmark harnesses" + sha256sum "$SOURCE/a_public_endpoints.c" "$SOURCE/b_actual_h.c" \ + "$SOURCE/summarize.py" "$SOURCE/run_limited.sh" +} > logs/env.txt 2>&1 +cat logs/env.txt + +# --- build ---------------------------------------------------------------- +{ + echo "### building a_public_endpoints" + set -x + cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \ + "$SOURCE/a_public_endpoints.c" $SRC_COMMON -lm -o "$BIN/a_public" + echo "### building b_actual_h" + cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \ + "$SOURCE/b_actual_h.c" "$root/src/asymptotic.c" \ + "$root/src/asymptotic_schwarzschild.c" "$root/src/spacetime_common.c" \ + "$root/src/observer.c" -lm -o "$BIN/b_actual" + set +x +} > logs/build.log 2>&1 +cat logs/build.log + +# --- Experiment A --------------------------------------------------------- +{ + echo "### command: a_public schwarzschild" + set -x; time "$BIN/a_public" schwarzschild; set +x + echo "### command: a_public minkowski" + set -x; time "$BIN/a_public" minkowski; set +x + echo "### command: a_public alcubierre" + set -x; time "$BIN/a_public" alcubierre; set +x +} > logs/expA.log 2>&1 +grep -E '^(SUMMARY|TOTAL_RAYS|real|user)' logs/expA.log | tail -20 + +# --- Experiment B --------------------------------------------------------- +{ + echo "### command: b_actual" + set -x; time "$BIN/b_actual"; set +x +} > logs/expB.log 2>&1 +cat logs/expB.log + +# --- summary tables ------------------------------------------------------- +python3 "$SOURCE/summarize.py" "$OUT" + +echo +echo "rays A = $(grep '^TOTAL_RAYS' logs/expA.log | awk '{s+=$2} END{print s}')" +echo "observed steps B = $(grep '^TOTAL_OBSERVED_STEPS' logs/expB.log | awk '{print $2}')" +echo "outputs under $OUT" diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py b/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py new file mode 100755 index 0000000..18c0a5e --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Bounded 320x180 adaptive-mesh candidates (8 cases; no 4K, no references). + +Usage: + run_mesh.py [OUT_DIR] + +OUT_DIR defaults to /local/adaptive_bounds_mesh. The renderer binary is +expected to already exist at build/Release/schwarzschild_sky; build it with +(make command is also in README.md): + + make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend + +No survey catalog is used: the script writes a one-point synthetic dim catalog +with the required column header (longitude_deg,latitude_deg,temperature_K, +amplitude) and a 1e-30 amplitude star. No external CSV is read. +""" +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import time + +from mesh_maps import load, compare # tracked sibling module (no local/ import) + +ROOT = Path(__file__).resolve().parents[2] +OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \ + ROOT / 'local/adaptive_bounds_mesh' +BIN = ROOT / 'build/Release/schwarzschild_sky' +BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend' +ENV = dict(os.environ, OMP_NUM_THREADS='8', OMP_DYNAMIC='FALSE') + + +def scene_args(scene): + if scene == 'r100': + return ['--observer-radius', '100', '--look-ra-deg', '262.5', + '--look-dec-deg', '-30', '--fov-deg', '45', + '--refine-jacobian-min', '.2'] + return ['--observer-position', '2.1', '0', '0', '--observer-velocity', + '0', '0', '0', '--look-ra-deg', '0', '--look-dec-deg', '0', + '--fov-deg', '90'] + + +def main(): + if not BIN.exists(): + raise SystemExit(f'missing {BIN}; build it first:\n {BUILD_CMD}') + OUT.mkdir(parents=True, exist_ok=True) + catalog = OUT / 'single_dim_star.csv' + catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n' + '262.5,-30,6000,1e-30\n') + results = {} + for scene in ('r100', 'r2p1'): + meshes = {} + for upper in (.5, 2, 8, 32): + name = f'{scene}_hmax{upper:g}' + cmd = [str(BIN), '--integrator', 'dp54', + '--width', '320', '--height', '180', + '--coarse-cell-pixels', '8', '--refine-max-level', '3', + '--catalog', str(catalog), '--exposure', '1', + '--psf-min-y', '1e-20', '--psf-relative-tail', '1e-4', + '--ode-min-step', '1e-12', '--ode-max-step', str(upper), + '--verbose', '--lens-map-output', str(OUT / (name + '.grlens')), + '--output', str(OUT / (name + '.png')), *scene_args(scene)] + start = time.perf_counter() + with (OUT / (name + '.log')).open('w') as log: + log.write('OMP_NUM_THREADS=8 OMP_DYNAMIC=FALSE ' + + shlex.join(cmd) + '\n') + log.flush() + subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log, + stderr=subprocess.STDOUT, check=True, timeout=180) + info, meshes[upper] = load(OUT / (name + '.grlens')) + info['wall_seconds'] = time.perf_counter() - start + results[name] = info + print(name, json.dumps(info), flush=True) + for upper in (.5, 8, 32): + comparison = compare(meshes[2], meshes[upper]) + results[f'{scene}_2_vs_{upper:g}'] = comparison + print(f'{scene} 2 vs {upper:g}', json.dumps(comparison), flush=True) + (OUT / 'summary.json').write_text(json.dumps(results, indent=2) + '\n') + + +if __name__ == '__main__': + main() diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py b/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py new file mode 100755 index 0000000..2c84ebf --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Two bounded tight-tolerance mesh references and comparisons (no 4K). + +Usage: + run_mesh_reference.py [OUT_DIR] + +OUT_DIR defaults to /local/adaptive_bounds_mesh and must already contain +the eight ``_hmax.grlens`` maps from ``run_mesh.py``. The renderer +binary is expected at build/Release/schwarzschild_sky (see README for the make +command). Same one-point synthetic dim catalog, no external CSV. +""" +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import time + +from mesh_maps import load, compare + +ROOT = Path(__file__).resolve().parents[2] +OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \ + ROOT / 'local/adaptive_bounds_mesh' +BIN = ROOT / 'build/Release/schwarzschild_sky' +BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend' +ENV = dict(os.environ, OMP_NUM_THREADS='8', OMP_DYNAMIC='FALSE') + + +def scene_args(scene): + if scene == 'r100': + return ['--observer-radius', '100', '--look-ra-deg', '262.5', + '--look-dec-deg', '-30', '--fov-deg', '45', + '--refine-jacobian-min', '.2'] + return ['--observer-position', '2.1', '0', '0', '--observer-velocity', + '0', '0', '0', '--look-ra-deg', '0', '--look-dec-deg', '0', + '--fov-deg', '90'] + + +def main(): + if not BIN.exists(): + raise SystemExit(f'missing {BIN}; build it first:\n {BUILD_CMD}') + OUT.mkdir(parents=True, exist_ok=True) + catalog = OUT / 'single_dim_star.csv' + if not catalog.exists(): + catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n' + '262.5,-30,6000,1e-30\n') + results = {} + for scene in ('r100', 'r2p1'): + name = scene + '_tight_reference' + cmd = [str(BIN), '--integrator', 'dp54', '--width', '320', + '--height', '180', '--coarse-cell-pixels', '8', + '--refine-max-level', '3', '--catalog', str(catalog), + '--exposure', '1', '--psf-min-y', '1e-20', + '--psf-relative-tail', '1e-4', '--ode-min-step', '1e-12', + '--ode-max-step', '.5', '--ode-rtol', '1e-12', + '--ode-atol-x', '1e-12', '--ode-atol-pi', '1e-12', + '--ode-atol-l', '1e-12', '--verbose', + '--lens-map-output', str(OUT / (name + '.grlens')), + '--output', str(OUT / (name + '.png')), *scene_args(scene)] + start = time.perf_counter() + with (OUT / (name + '.log')).open('w') as log: + log.write('OMP_NUM_THREADS=8 OMP_DYNAMIC=FALSE ' + + shlex.join(cmd) + '\n') + log.flush() + subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log, + stderr=subprocess.STDOUT, check=True, timeout=180) + info, ref = load(OUT / (name + '.grlens')) + info['wall_seconds'] = time.perf_counter() - start + results[name] = info + print(name, json.dumps(info), flush=True) + for upper in (.5, 2, 8, 32): + _, vertices = load(OUT / f'{scene}_hmax{upper:g}.grlens') + result = compare(ref, vertices) + results[f'{scene}_ref_vs_{upper:g}'] = result + print(scene, 'ref vs', upper, json.dumps(result), flush=True) + (OUT / 'reference_summary.json').write_text( + json.dumps(results, indent=2) + '\n') + + +if __name__ == '__main__': + main() diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/summarize.py b/benchmarks/adaptive_step_bounds_2026-10-05/summarize.py new file mode 100644 index 0000000..80f3a56 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/summarize.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Summarise the adaptive-step-bounds benchmark raw CSV logs. + +Usage: + summarize.py [OUT_DIR] + +Reads ``OUT_DIR/raw/*.csv`` (written by ``run_limited.sh``) and writes +``OUT_DIR/logs/summary.txt``. It has no dependency on any untracked script or +on older local data. + +Defaults: OUT_DIR = /local/adaptive_step_bounds_2026-10-05, derived from +this file's location (benchmarks/adaptive_step_bounds_2026-10-05/). +""" +import csv +import math +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) +DEFAULT_OUT = os.path.join(ROOT, "local", "adaptive_step_bounds_2026-10-05") + +OUTCOME = {0: "ESC", 1: "DARK", 2: "UNRES", 3: "INC"} +REASON = { + 0: "NONE", + 1: "REDSHIFT", + 2: "BUDGET", + 3: "TIMERANGE", + 4: "DOMAIN", + 5: "INVMETRIC", + 6: "INTEGERR", + 7: "UNSUPPORTED", + 8: "PROTOCOL", + 9: "IO", +} + + +def fnum(v): + if v is None or v == "" or v == "nan": + return math.nan + return float(v) + + +def fmt(v): + if isinstance(v, float): + if math.isnan(v): + return "nan" + if v == 0: + return "0" + return f"{v:.4g}" + return str(v) + + +def table(title, header, recs): + lines = [f"## {title}", ""] + lines.append("| " + " | ".join(header) + " |") + lines.append("|" + "|".join(["---"] * len(header)) + "|") + for r in recs: + lines.append("| " + " | ".join(fmt(x) for x in r) + " |") + lines.append("") + return lines + + +def ang_between(a, b): + va = [fnum(a["nx"]), fnum(a["ny"]), fnum(a["nz"])] + vb = [fnum(b["nx"]), fnum(b["ny"]), fnum(b["nz"])] + na = math.sqrt(sum(x * x for x in va)) + nb = math.sqrt(sum(x * x for x in vb)) + if not (na > 0 and nb > 0): + return math.nan + cross = (va[1]*vb[2]-va[2]*vb[1], + va[2]*vb[0]-va[0]*vb[2], + va[0]*vb[1]-va[1]*vb[0]) + dot = sum(x * y for x, y in zip(va, vb)) + return math.atan2(math.sqrt(sum(x*x for x in cross)), dot) + + +class Raw: + def __init__(self, out_dir): + self.raw = os.path.join(out_dir, "raw") + self.logs = os.path.join(out_dir, "logs") + + def rows(self, name): + with open(os.path.join(self.raw, name)) as f: + return list(csv.DictReader(f)) + + +def summarise_agg(raw, name, title, keycol): + recs = raw.rows(name) + out = [] + for r in recs: + out.append((r["case"], float(r[keycol]), int(r["class_mismatch"]), + fnum(r["max_dn_ang"]), fnum(r["max_dgrel"]), + fnum(r["max_dstopT"]), int(r["sum_rhs"]), + int(r["max_rejected"]), int(r["escaped"]), int(r["dark"]), + int(r["unresolved"]), int(r["incomplete"]))) + out.sort(key=lambda x: (x[0], x[1])) + return table(title, + ["case", keycol, "mismatch", "max_dn_ang", "max_dgrel", + "max_dstopT", "sum_rhs", "max_rej", "ESC", "DARK", "UNRES", + "INC"], out) + + +def build(raw): + lines = ["# Adaptive-step-bounds benchmark summary tables", ""] + + ref = raw.rows("a_sch_reference.csv") + recs = [] + for r in ref: + recs.append((r["case"], int(r["dir"]), fnum(r["theta"]), + OUTCOME[int(r["outcome"])], REASON[int(r["reason"])], + fnum(r["stop_t"]), int(r["steps"]), int(r["rejected"]), + int(r["rhs"]), fnum(r["g"]), fnum(r["thr"]))) + lines += table("A Schwarzschild references (DP tol=1e-12, max_step=0.25)", + ["case", "dir", "theta", "outcome", "reason", "stop_t", + "steps", "rej", "rhs", "g", "thr"], recs) + + lines += summarise_agg(raw, "a_sch_upper_summary.csv", + "A Schwarzschild upper scan (min_step=1e-12, " + "tol=1e-9)", "upper") + lines += summarise_agg(raw, "a_sch_min_summary.csv", + "A Schwarzschild min scan (max_step=2, tol=1e-9)", + "min_step") + + rk = raw.rows("a_sch_rk4_sensitive.csv") + groups = {} + for r in rk: + groups.setdefault((r["case"], r["dir"]), {})[r["phase"]] = r + recs = [] + for (case, d), gg in sorted(groups.items()): + a = gg.get("rk4_0.01") + b = gg.get("rk4_0.005") + if not a or not b: + continue + dn = math.nan if a["outcome"] != b["outcome"] else ang_between(a, b) + dg = (math.nan if a["outcome"] != b["outcome"] + else abs(fnum(a["g"]) - fnum(b["g"]))) + recs.append((case, d, fnum(a["theta"]), OUTCOME[int(a["outcome"])], + OUTCOME[int(b["outcome"])], dn, dg, int(a["rhs"]), + int(b["rhs"]))) + lines += table("A Schwarzschild sensitive RK4 .01 vs .005", + ["case", "dir", "theta", "out.01", "out.005", + "hhalve_dn_ang", "|dg|", "rhs.01", "rhs.005"], recs) + + mk = raw.rows("a_mink_analytic.csv") + mkx = raw.rows("a_mink_analytic_x.csv") + groups = {} + for r in mk: + groups.setdefault((r["upper"], r["min_step"]), []).append(r) + recs = [] + for (u, f), g in sorted(groups.items(), + key=lambda kv: (float(kv[0][0]), float(kv[0][1]))): + recs.append((u, f, len(g), max(fnum(r["dn_ang"]) for r in g), + max(fnum(r["dgrel"]) for r in g), + max(fnum(r["t_err"]) for r in g))) + lines += table("A Minkowski flat analytic check (n_inf and t)", + ["upper", "min_step", "n", "max_dn_ang", "max_dgrel", + "max_t_err"], recs) + groups = {} + for r in mkx: + groups.setdefault((r["upper"], r["min_step"]), []).append(r) + recs = [] + for (u, f), g in sorted(groups.items(), + key=lambda kv: (float(kv[0][0]), float(kv[0][1]))): + maxx = max(max(fnum(r["x_err_x"]), fnum(r["x_err_y"]), + fnum(r["x_err_z"])) for r in g) + recs.append((u, f, len(g), maxx, max(fnum(r["t_err"]) for r in g))) + lines += table("A Minkowski flat analytic check (crossing x and t)", + ["upper", "min_step", "n", "max_x_err", "max_t_err"], recs) + + for case in ["alc_v3_s1", "alc_v3_s10", "alc_v9_s1", "alc_v9_s10"]: + lines += summarise_agg(raw, f"a_alc_{case}_upper_summary.csv", + f"A Alcubierre {case} upper scan", "upper") + lines += summarise_agg(raw, "a_alc_v9_s100_min_summary.csv", + "A Alcubierre v9 s100 lower-bound stress " + "(upper=0.004)", "min_step") + + b = raw.rows("b_summary.csv") + recs = [] + for r in b: + recs.append((r["case"], int(r["dir"]), + OUTCOME[int(r["ref_outcome"])], fnum(r["span"]), + fnum(r["target"]), int(r["observed_steps"]), + int(r["reached_target"]), + REASON.get(int(r["terminated_reason"]), + r["terminated_reason"]), + fnum(r["h_min"]), fnum(r["h_max"]), fnum(r["h_first"]), + int(r["boundary_steps"]), int(r["sum_rhs"]), + int(r["sum_reject"]), fnum(r["null_residual_max"]))) + lines += table("B observed accepted step h (finite T=min(20, ref span))", + ["case", "dir", "ref", "span", "target", "steps", "reached", + "term", "h_min", "h_max", "h_first", "bnd", "rhs", "rej", + "null_max"], recs) + + if os.path.exists(os.path.join(raw.raw, "critical_ref.csv")): + lines += critical_section(raw) + return lines + + +def critical_section(raw): + rows = raw.rows("critical_ref.csv") + key = {} + for r in rows: + key[(r["camera"], int(r["dir"]), r["cfg"])] = r + lines = table("critical reference check (raw rows)", + ["camera", "dir", "theta", "cfg", "outcome", "reason", + "end_id", "stop_t", "steps", "rejected", "rhs", "g", "thr", + "L", "L0"], + [(r["camera"], int(r["dir"]), fnum(r["theta"]), r["cfg"], + OUTCOME[int(r["outcome"])], REASON[int(r["reason"])], + r["end_id"], fnum(r["stop_t"]), int(r["steps"]), + int(r["rejected"]), int(r["rhs"]), fnum(r["g"]), + fnum(r["thr"]), fnum(r.get("L")), fnum(r.get("L0"))) + for r in rows]) + # pair comparisons + pairs = [("ref_tol1e-12_h0.25", "ref_tol1e-13_h0.125"), + ("tol1e-11_h2", "tol1e-11_h8"), + ("ref_tol1e-12_h0.25", "tol1e-11_h2"), + ("ref_tol1e-12_h0.25", "tol1e-11_h8")] + for a_cfg, b_cfg in pairs: + recs = [] + for (cam, di, cfg), a in sorted(key.items()): + if cfg != a_cfg: + continue + b = key.get((cam, di, b_cfg)) + if not b: + continue + same = a["outcome"] == b["outcome"] + dn = ang_between(a, b) if same else math.nan + dg = (math.nan if not same else + abs(fnum(a["g"]) / fnum(b["g"]) - 1.0) + if fnum(a["g"]) > 0 and fnum(b["g"]) > 0 else math.nan) + dstop = (fnum(a["stop_t"]) - fnum(b["stop_t"]) + if fnum(a["stop_t"]) == fnum(a["stop_t"]) and + fnum(b["stop_t"]) == fnum(b["stop_t"]) else math.nan) + dmar = (fnum(a["thr"]) - fnum(b["thr"]) + if fnum(a["thr"]) == fnum(a["thr"]) and + fnum(b["thr"]) == fnum(b["thr"]) else math.nan) + recs.append((cam, di, fnum(a["theta"]), + OUTCOME[int(a["outcome"])], OUTCOME[int(b["outcome"])], + same, dn, dg, dstop, dmar)) + lines += table(f"critical pair {a_cfg} vs {b_cfg}", + ["camera", "dir", "theta", "a", "b", "same", + "dn_ang", "dgrel", "dstop_t", "dthr"], recs) + return lines + + +def main(argv): + out = argv[1] if len(argv) > 1 else DEFAULT_OUT + raw = Raw(out) + os.makedirs(raw.logs, exist_ok=True) + lines = build(raw) + path = os.path.join(raw.logs, "summary.txt") + with open(path, "w") as f: + f.write("\n".join(lines) + "\n") + print(f"wrote {path} ({len(lines)} lines)") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/benchmarks/adaptive_step_bounds_2026-10-05/summarize_4k.py b/benchmarks/adaptive_step_bounds_2026-10-05/summarize_4k.py new file mode 100755 index 0000000..63783b3 --- /dev/null +++ b/benchmarks/adaptive_step_bounds_2026-10-05/summarize_4k.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Postprocess existing 4K/limited maps only; never invokes a renderer. + +Usage: + summarize_4k.py [OUT_DIR] + +OUT_DIR defaults to /local/adaptive_bounds_4k and must contain +``r100_4k_hmax2.grlens``, ``r100_4k_hmax8.grlens`` and ``attempts.json``. +Writes summary.json in OUT_DIR. + +Cost scope note: accepted/rejected/RHS sums cover the final persistent map +vertices only. Nonpersistent discarded refinement probes are not stored in the +v3 map, so these sums are not full-render executed RHS totals. +""" +import json +import re +from pathlib import Path +import sys + +from mesh_maps import load, compare + +ROOT = Path(__file__).resolve().parents[2] +OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \ + ROOT / 'local/adaptive_bounds_4k' + + +def main(): + summaries = {} + meshes = {} + for upper in (2, 8): + name = f'r100_4k_hmax{upper}' + summaries[name], meshes[upper] = load(OUT / (name + '.grlens')) + text = (OUT / (name + '.log')).read_text() + initial = int(re.search(r'tracing (\d+) initial rays', text).group(1)) + generations = [int(n) for n in re.findall( + r'refinement generation \d+ tracing (\d+) samples', text)] + summaries[name]['requested_samples_including_nonpersistent_probes'] = \ + initial + sum(generations) + summaries[name]['initial_samples'] = initial + summaries[name]['refinement_samples_per_generation'] = generations + summaries[name]['trace_wall_seconds'] = sum( + float(x) for x in re.findall( + r'(?:initial ray trace|adaptive ray-trace refinement) ' + r'finished in ([\d.]+) s', text)) + ledger = OUT / 'attempts.json' + if ledger.exists(): + for entry in json.loads(ledger.read_text()): + if entry.get('state') == 'completed': + summaries[f'r100_4k_hmax{entry["upper"]}']['wall_seconds'] = \ + entry['wall_seconds'] + summaries['comparison'] = compare(meshes[2], meshes[8]) + a, b = (summaries[f'r100_4k_hmax{upper}'] for upper in (2, 8)) + summaries['relative_reduction'] = { + key: 1 - b[key] / a[key] + for key in ('accepted', 'rhs', 'wall_seconds', 'trace_wall_seconds')} + summaries['comparison']['triangle_payload_identical'] = \ + a['triangle_sha256'] == b['triangle_sha256'] + summaries['cost_scope'] = ( + 'accepted/rejected/RHS sums cover final persistent vertices only; ' + 'nonpersistent discarded refinement probes are not in the v3 map. ' + 'Do not interpret them as full-render executed RHS totals.') + (OUT / 'summary.json').write_text(json.dumps(summaries, indent=2) + '\n') + print(json.dumps(summaries, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/mk/reference_images.mk b/mk/reference_images.mk index fa100ec..5184ee6 100644 --- a/mk/reference_images.mk +++ b/mk/reference_images.mk @@ -1,5 +1,5 @@ REFERENCE_DIR := tests/data/psf_event_sink_reference -REFERENCE_TMP_DIR := /tmp/gr_psf_event_sink_reference +REFERENCE_TMP_DIR := /tmp/opencode/gr_psf_event_sink_reference FLOATDIFF_SCRIPT := scripts/fits_floatdiff.py .PHONY: test-reference-images @@ -9,8 +9,8 @@ test: test-reference-images test-reference-images: $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits $(MAKE) SPACETIME=minkowski ENABLE_HDR=1 backend mkdir -p $(REFERENCE_TMP_DIR) - OMP_NUM_THREADS=16 $(BUILD_DIR)/minkowski_sky --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 30 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4 + OMP_NUM_THREADS=16 $(BUILD_DIR)/minkowski_sky --integrator rk4 --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 30 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4 python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(MAKE) SPACETIME=schwarzschild ENABLE_HDR=1 backend - OMP_NUM_THREADS=16 $(BUILD_DIR)/schwarzschild_sky --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 60 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4 + OMP_NUM_THREADS=16 $(BUILD_DIR)/schwarzschild_sky --integrator rk4 --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 60 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4 python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits diff --git a/nr_spacetime_movie_renderer_design.md b/nr_spacetime_movie_renderer_design.md index abcfe3f..616471d 100644 --- a/nr_spacetime_movie_renderer_design.md +++ b/nr_spacetime_movie_renderer_design.md @@ -954,35 +954,34 @@ residual、Chebyshev 表或解析主项;运行期不得建表。 - 高精度 reference evaluator 在域内不收敛; - 表在 seam 或 grazing 区域超过误差限。 -## 18A.10 当前实现状态 +## 18A.10 Backend 能力与路由约束 -- 公共接口(end descriptor、escape worldtube sample、canonical photon state 与 - endpoint `end_id`)已实现; +- 公共接口以 end descriptor、escape worldtube sample、canonical photon state 与 + endpoint `end_id` 分离远端声明、几何事件、状态转接与终态来源; - $M=0$ 固定球与匀速移动球的 camera pre-route、directed inside -> outside - crossing、`PENDING_ENTRY` 生命周期已实现,并接入 Minkowski 与 Alcubierre; -- 单帧与 movie 共用同一 pre-route/生命周期实现;`make test` 全绿; + crossing 与 `PENDING_ENTRY` 生命周期采用同一几何约定,适用于 Minkowski 与 + Alcubierre 的平直外区; +- 单帧与 movie 必须共用同一 pre-route/生命周期实现; - $M>0$ 固定同心 Schwarzschild monopole 外区以**解析 Carlson 椭圆积分**实现 (见 18A.11),接入解析 Kerr–Schild backend;相机位于 escape 球外的 pre-route、entry 传播、directed crossing 与 infinity endpoint 均可用; -- 任意加速 worldtube 的 bracketed root driver 已实现并由 synthetic accelerated - worldtube 测试覆盖(含跨 motion-segment 与 history-exhausted failure - semantics),但尚无生产 backend 使用该路径。该 driver 只检查离散端点的 - $F$ 符号,尚不能保证捕获一个步长内的窄进入;任意加速 backend 落地前需加入 - 段内速度/加速度界或自适应子区间搜索。 -- 多渐近远端路由尚未实现:`end_id` 已进入 endpoint、`LensVertex`、 - `terminal_mismatch` 与 `discrete_jacobian`,避免跨 end 插值;但 - `asymptotic_route_camera` 目前对多个 end 只处理第一个,lens-map 文件格式也 - 未序列化 `end_id`。虫洞/最大延拓接入前需要补这些。 +- worldtube 的运动能力必须显式声明。分段匀速、线性半径变化可在各段内求首次 + crossing,并在段边界重新采样;任意加速运动在没有可信区间界时必须报告 + `UNSUPPORTED`,不得把离散采样没有发现根解释为正常 miss。扩展加速运动能力需要 + 同时提供可信的段内界或专用首次 crossing 解法。 +- `end_id` 贯通 endpoint、`LensVertex`、`terminal_mismatch` 与 + `discrete_jacobian`,避免跨 end 插值;lens-map 保存 + `end_id`。不同物理 region 的 reachable-end 路由及 catalog 绑定仍需独立定义, + 不能把能够保存 end 标识解释为已支持完整解析延拓。 - 生命周期为三态:`NO_ENDS`(才允许 legacy)、`DIRECTED_READY`、 `PROTOCOL_ERROR`(descriptor 读取失败或不支持的 exterior,显式 `INVALID/UNSUPPORTED`,绝不退回 legacy)。pre-route 也先校验所有 descriptor 与 exterior kind,再判定 worldtube 内外,因此“声明了不受支持 exterior 而相机 恰在球内”不会被静默接受。 - 内区积分时若 worldtube sample 变为 `valid == 0`(历史耗尽),作为一等 - terminal reason 返回 `RAY_ENDPOINT_TIME_RANGE_EXHAUSTED`、保留 `end_id`,并由 - `RayPool` 记为 `RAY_POOL_TERMINATED`,与 pre-route 的耗尽语义一致。generic - accelerated driver 与内区 crossing localizer 的二分过程中任何 sample 失败都 - 直接传播该具体状态,不返回一个正常 entry 或普通 integration failure。 + terminal reason 返回 `INCOMPLETE/TIME_RANGE_EXHAUSTED`、保留 `end_id`,并由 + `RayPool` 记为 `RAY_POOL_TERMINATED`,与 pre-route 的耗尽语义一致。内区 crossing + localizer 定位过程中任何 sample 失败都直接传播具体状态,不返回一个正常 entry。 - Minkowski entry quadratic 用稳定根公式($q=-\tfrac12(b+\mathrm{copysign} (\sqrt\Delta,b))$,取最小正根);$c=0$(相机在边界)时按 $b=\mathrm dF/ \mathrm ds$ 分类。worldtube sample 必须有限且 $R>0$,否则 `INVALID`。 @@ -1076,6 +1075,104 @@ residual、Chebyshev 表或解析主项;运行期不得建表。 --- +# 18B. 自适应测地线积分 + +积分器负责轨迹误差控制、拒步与数值失败诊断,不承担物理终态分类。步长、计算配额与 +回溯时间是独立配置;改变步长或容差不得隐式改变可追踪的时间范围。 + +- 生产默认采用 DP54;固定 RK4 保留为显式数值对照。CLI 默认相对容差与 `Pi/L` + 绝对容差为 `1e-9`,位置绝对容差为 `1e-9` 倍 backend 长度单位(解析 + Schwarzschild 为 `M`,Alcubierre 为 bubble 半径,Minkowski 为坐标单位长度)。 + 初始步长、最大步长及时间配额按 backend 的长度/时间单位配置,所有参数均可显式 + 覆盖并写入 provenance。容差控制的是局部 ODE 误差,不承诺临界光线最终方向的 + 全局误差界;生产精度应以独立容差与几何尺度收敛确定。 +- CLI 步长下限为 `max(1e-12,16*DBL_EPSILON)` 倍 backend 长度单位(`c=1`), + 是不绑定正常轨迹的数值保护,不是已测得的物理最小时间尺度。上限在解析 + Schwarzschild 为 `8M`,Minkowski 为 `16` 个坐标时间单位,Alcubierre 为 + 初始默认步长的八倍。上限用于限制 trial 区间,不替代误差与首次事件控制; + near-critical 光线应独立收紧容差,不能只靠减小上限保证分类与方向收敛。 + 参数扫描与复现输入见 `benchmarks/adaptive_step_bounds_2026-10-05/`;解析 backend + 的测量不外推为 NR/DG 数据的默认步长标定。 +- `GeodesicTraceConfig.stepper` 区分固定 RK4 与 Dormand–Prince 5(4)。DP54 的 + `atol_x/atol_Pi/atol_L/rtol`、`min_step/max_step`、 + `consecutive_rejection_limit`、`max_lookback_time` 全部显式;缺省、非正、非有限或 + `min_step > max_step` 直接返回 protocol error,不偷偷填默认值。初始 trial step + 复用 `coordinate_time_step`。 +- DP54 采用标准 Dormand–Prince 5(4)7M tableau,接受 5 阶解,误差用 + `h*sum((B5-B4)*k)` 直接计算;位置误差尺度 + `atol_x + rtol*max(|dx|,|h*k1.x|)`,`Pi` 用 `atol_Pi + rtol*max(|Pi_before|,|Pi_candidate|)`, + `L` 用 `atol_L + rtol*max(1,|dL|)`;控制器 safety `0.9`、factor clamp `[0.2,5]`、 + exponent `1/5`,无 FSAL(每次 trial 7 次 RHS),拒步不提交状态。 +- 自适应控制状态属于各条 ray:保存 `integration_start_time`(activation/entry 时间, + 非相机能量参考 `L0`)、`next_step`、累计 rejected/RHS 计数与 `previous_rejected`; + `steps` 表示 accepted 步数。状态须跨 slab 与预算重试保留;endpoint 保存最后可信 + 轨迹及控制/成本状态,使续追无需从相机重放,也不重置 `L0`。 +- 控制/成本状态在 scheduler 各层按值传递:batch 路径的 `RayPool` SoA 保存同一组 + per-ray 字段,slab activation 只重置新 ray 的 accepted 步数,绝不重置 adaptive + control state;frame/movie 的续追请求以完整 resume state 重建(frame 与 movie + 共用同一 helper,禁止逐字段手抄或漏字段)。endpoint 记录本次实际授予的 time + budget,供 retry 层独立累计。off-mesh witness 原地提升必须整体复制该状态, + 重试回填保留连续状态与成本计数。 +- 计算配额与回溯时间是两条独立、各自饱和的 retry 预算:step 增量与 time 增量互不 + 替代,只有"实际耗尽且仍可增长"的 quota 才允许发起 retry;任一 blocking quota 已 + 达硬上限时不得仅凭另一 quota 产生无效重试,直接按 budget-incomplete 处理。两个 + quota 的实际配置总 grant 必须与已 spent 数量分开记录,retry 采样取实际 grant, + 不得由已花费步数反推授权。"可增长"指新 quota 必须打开严格更大的可表示区间:整数 + 步长严格增加,time 左边界 `start - quota` 严格前移;因大 time origin 或增量过小 + 而 round 到同一区间的增量不算有效增长,不得发起永远无法推进的 retry。两者都到上限 + 后 UUU 仍为 budget-incomplete,UUD/UDD 在几何停止尺度可按 finite-resolution 近似 + 标黑。自动补齐的 time 重试增量由 `max_lookback_time` 指定,总上限为其四倍 + (对有限数值范围饱和);这是可覆盖的 resource quota policy,不是物理终态条件。 +- 每步对 slab 左边界与显式回溯预算左边界截断;DP54 不以 + `coordinate_time_step * max_steps` 定义时间范围。回溯预算耗尽而轨迹仍可信 + 返回 `UNRESOLVED/BUDGET_EXHAUSTED`;真实的 slab data time failure 仍是 + `INCOMPLETE/TIME_RANGE_EXHAUSTED`。`max_steps` 仍作为 accepted 步数额度。 + 各 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`。 + 拒步上限耗尽时保留具体 stage 原因,或报告积分误差不可控。失败路径保证调用者 + state 与 endpoint 的最后可信状态一致。 +- directed inside→outside 事件的 DP 子积分定位使用同一 DP 误差控制(不混用 RK4), + 成本计入 endpoint 但不伪造 accepted 主步。定位的 time bracket + 停止容差取局部步长量级与真实 `nextafter` ULP 的组合(不随远端 `|t|` 放大),任何 + 可表示的非零 target−before 区间都实际积分,只有 target==before 才允许直接取状态; + `t+h==t` 明确失败或返回已接受可信状态,不伪造轨迹。子积分失败经独立 RayReason 透传 + (`TIME_UNAVAILABLE→TIME_RANGE_EXHAUSTED`、`INVALID_METRIC`、`OUT_OF_DOMAIN`、 + `INTEGRATION_ERROR`),与 worldtube descriptor 自身的 protocol/history 失败区分。 +- 嵌入误差估计不保证找到全部几何事件;窄进出、first-crossing 与同一步内多个事件的 + 顺序必须独立验证,不能只凭步端点符号或误差通过认定事件完整。积分精度还须通过 + 逃逸方向、频移、null 残差及适用 backend 的独立守恒量验证;不靠反复归一化掩盖误差。 +- DP54 accepted 区间的四次 dense interpolant 用于枚举事件候选。对分段匀速球形 + worldtube,代入 `F=|x-center|²-radius²` 得到至多八次多项式;通过导数根划分 + 单调区间并隔离根,不能仅查看主步两端符号。根按过去传播方向排序,区分真正的 + inside→outside crossing 与切触;motion 段边界截断主步并重新建立候选。 + 相机相对能量阈值的候选从同一 accepted 区间的 `L-L0` 获得。候选必须经误差受控 + 子积分验证,escape 与暗阈值按可信轨迹顺序处理;数值不可分辨的同时事件采用明确、 + 一致的优先规则,不能由 end descriptor 的排列决定终态。 + `L-L0` 在动态时空中不假定单调:即使主步两端均低于阈值,也要处理步内的首次 + upcrossing。阈值根按导数根分段,在局部 bracket 内确认并定位;不能用整个区间的 + 二分取代首次 crossing。所有候选的可信时间须在子积分后重新比较;当前不可分辨的 + 同时事件取 escape 优先。阈值是 `>=` 的闭事件,真正的步末 crossing 不能因根 + 合并到端点而丢失。无法确认事件时有界缩步,耗尽后报告积分失败,不回退为假成功。 + `LOG_P0` 辅助监测策略保留 accepted-state 检查,不提供这一多项式事件定位能力; + 生产 CLI 使用相机相对 `L-L0` 策略。 +- lens-map 的积分 provenance 必须保存稳定的 stepper wire code、分组绝对/相对 + 容差、初始步长及上下限、拒步上限、初始计算/回溯配额与重试政策。render-only + replay 消费文件策略,不能由当前 CLI 默认值覆盖历史配置,也不从缺失字段推断 + 自适应积分来源。未知 stepper code 或无效配置须拒绝;只含固定 RK4 来源的旧文件 + 可按明确的固定步语义导入,但不能补成 DP54 结果。 +- accepted、rejected 与实际 RHS 成本按持久 sample 身份统计;续追保存累计值,回填 + 替换而不是再次累加。off-mesh witness 与原地提升后的顶点是同一样本,不能重复 + 计费;事件子积分和失败 stage 的 RHS 也计入真实成本。渲染地图保存这些诊断值, + replay 的来源与成本统计应与原始 trace 一致。 + +--- + # 19. 恒星 catalog 的内部表示 不要预存 RGB。 diff --git a/src/asymptotic.c b/src/asymptotic.c index 8bc9b6a..8e27f2e 100644 --- a/src/asymptotic.c +++ b/src/asymptotic.c @@ -257,106 +257,6 @@ static int solve_entry_quadratic(const double d[3], const double q[3], return 1; } -static double worldtube_F_frame(const double c_frame[3], double radius, - const double x_frame[3]) { - const double d[3] = {x_frame[0] - c_frame[0], x_frame[1] - c_frame[1], - x_frame[2] - c_frame[2]}; - return dot3(d, d) - radius * radius; -} - -/* Bracketed first-entry search for a worldtube whose motion is not constant. - * This is the future interface for accelerated worldtubes; the current - * backends always take the closed quadratic path above. */ -static AsymptoticStatus minkowski_generic_first_entry( - const SpacetimeSource *source, const SpacetimeAsymptoticEnd *end, - double t0, const double x_frame[3], const double w_frame[3], - double *s_out) { - double s = 0.0; - SpacetimeEscapeWorldtubeSample sample; - AsymptoticStatus status = worldtube_sample(source, end, t0, &sample); - if (status != ASYMPTOTIC_OK) - return status; - double c_frame[3]; - backend_position_to_frame(end, sample.center, c_frame); - const double f_start = worldtube_F_frame(c_frame, sample.radius, x_frame); - if (f_start < 0.0) { - *s_out = 0.0; - return ASYMPTOTIC_OK; - } - if (f_start == 0.0) { - /* On the boundary only an inward slope is an entry; outward and tangent - * rays keep searching. */ - double v_frame[3], d0[3], q0[3]; - backend_vector_to_frame(end, sample.velocity, v_frame); - for (int i = 0; i < 3; ++i) { - d0[i] = x_frame[i] - c_frame[i]; - q0[i] = w_frame[i] + v_frame[i]; - } - const double slope0 = - 2.0 * dot3(d0, q0) + 2.0 * sample.radius * sample.radius_rate; - if (slope0 < 0.0) { - *s_out = 0.0; - return ASYMPTOTIC_OK; - } - } - const double speed = fabs(sample.velocity[0]) + fabs(sample.velocity[1]) + - fabs(sample.velocity[2]) + fabs(sample.radius_rate) + 1.0; - const double base_step = 0.5 * fmax(sample.radius, 1.0) / speed; - for (int iteration = 0; iteration < 1000000; ++iteration) { - double step = base_step; - const double boundary = spacetime_escape_worldtube_next_segment( - source, end->end_id, t0 - s); - if (isfinite(boundary)) { - /* The backward-integration distance to a past segment boundary. */ - const double to_boundary = (t0 - boundary) - s; - if (to_boundary > 0.0) - step = fmin(step, to_boundary); - } - const double s_next = s + step; - const double t_next = t0 - s_next; - SpacetimeEscapeWorldtubeSample next; - status = worldtube_sample(source, end, t_next, &next); - if (status != ASYMPTOTIC_OK) - return status; - double c_next[3], ray_next[3]; - backend_position_to_frame(end, next.center, c_next); - for (int i = 0; i < 3; ++i) - ray_next[i] = x_frame[i] + w_frame[i] * s_next; - const double f_next = worldtube_F_frame(c_next, next.radius, ray_next); - /* Only a strictly negative sample is an entry; a single touch at F == 0 - * (tangent) is not. */ - if (f_next < 0.0) { - double lo = s, hi = s_next; - AsymptoticStatus bisect_status = ASYMPTOTIC_OK; - for (int bisect = 0; bisect < 80; ++bisect) { - const double mid = 0.5 * (lo + hi); - SpacetimeEscapeWorldtubeSample mid_sample; - bisect_status = worldtube_sample(source, end, t0 - mid, &mid_sample); - if (bisect_status != ASYMPTOTIC_OK) - break; - double c_mid[3], ray_mid[3]; - backend_position_to_frame(end, mid_sample.center, c_mid); - for (int i = 0; i < 3; ++i) - ray_mid[i] = x_frame[i] + w_frame[i] * mid; - const double f_mid = - worldtube_F_frame(c_mid, mid_sample.radius, ray_mid); - if (f_mid <= 0.0) - hi = mid; - else - lo = mid; - } - /* A sample failure inside the bracket must not be disguised as a - * normal entry. */ - if (bisect_status != ASYMPTOTIC_OK) - return bisect_status; - *s_out = hi; - return ASYMPTOTIC_OK; - } - s = s_next; - } - return ASYMPTOTIC_INVALID; -} - static void minkowski_route_escaped(const SpacetimeAsymptoticEnd *end, const double w_frame[3], SpacetimeEndId end_id, @@ -405,14 +305,13 @@ static AsymptoticStatus minkowski_preroute( for (int i = 0; i < 3; ++i) x_cur[i] = x_frame[i] + w_frame[i] * s; if (!sample.velocity_constant) { - double s_rel; - status = minkowski_generic_first_entry(source, end, t, x_cur, w_frame, - &s_rel); - if (status != ASYMPTOTIC_OK) - return status; - minkowski_route_entry(end, t0, x_frame, w_frame, s + s_rel, end_id, - route); - return ASYMPTOTIC_OK; + /* An arbitrary accelerated worldtube has no strict interval bound on + * its relative motion, so a bracketed sign search can silently miss a + * narrow first entry. Until such bounds exist, report it explicitly + * instead of fabricating a route; piecewise-constant motion (the + * production backends) stays supported through the closed quadratic + * path below. */ + return ASYMPTOTIC_UNSUPPORTED; } double c_frame[3], v_frame[3], d[3], q[3]; backend_position_to_frame(end, sample.center, c_frame); diff --git a/src/frame.c b/src/frame.c index 46a3304..bd8e706 100644 --- a/src/frame.c +++ b/src/frame.c @@ -9,6 +9,7 @@ #include "optics.h" +#include #include #include #include @@ -102,6 +103,17 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height, return 0; } +/* Clamp an `unsigned long` cost counter into the persistent uint64_t field. + * On every platform this project builds for the two widths are equal, so the + * branch compiles away; it keeps a wider `unsigned long` from silently wrapping. */ +static uint64_t cost_counter_u64(unsigned long value) { +#if ULONG_MAX > UINT64_MAX + return value > (unsigned long)UINT64_MAX ? UINT64_MAX : (uint64_t)value; +#else + return (uint64_t)value; +#endif +} + /* Store an endpoint into a lens vertex. For an UNRESOLVED result the last * accepted continuous state is kept so the ray can be resumed; `granted_limit` * is the total accepted-step budget that produced this result (0 when it is @@ -112,6 +124,14 @@ static void store_endpoint(LensVertex *vertex, const RayEndpoint *endpoint, vertex->reason = endpoint->reason; vertex->end_id = endpoint->end_id; vertex->traced = 1; + /* Real integration cost for every outcome. The endpoint already reports the + * cumulative accepted/rejected/RHS totals of a resumed trace, so this is an + * overwrite, never an accumulation. A fresh (analytically pre-routed) escape + * legitimately reports zero. */ + vertex->trace_accepted_steps = (uint64_t)endpoint->accepted_steps; + vertex->trace_rejected_steps = (uint64_t)endpoint->rejected_steps; + vertex->trace_rhs_evaluations = cost_counter_u64(endpoint->rhs_evaluations); + vertex->trace_cost_saturated = 0; if (endpoint->outcome == RAY_OUTCOME_ESCAPED) { for (int axis = 0; axis < 3; ++axis) vertex->n_infinity[axis] = endpoint->n_infinity[axis]; @@ -127,13 +147,60 @@ static void store_endpoint(LensVertex *vertex, const RayEndpoint *endpoint, vertex->continuation_log_alpha_p0 = endpoint->final_log_alpha_p0; vertex->continuation_log_alpha_p0_0 = endpoint->final_log_alpha_p0_0; vertex->continuation_steps = endpoint->accepted_steps; + vertex->continuation_integration_start_time = + endpoint->integration_start_time; + vertex->continuation_next_step = endpoint->next_step; + vertex->continuation_rejected_steps = endpoint->rejected_steps; + vertex->continuation_rhs_evaluations = endpoint->rhs_evaluations; + vertex->continuation_previous_rejected = endpoint->previous_rejected; + /* Prefer the explicit grant of this request, then the trace's actual + * configured step budget. Only legacy/synthetic endpoints whose + * accepted_step_limit is zero fall back to the spent accepted count; real + * production must never infer a grant from steps already spent. */ const unsigned int used = - granted_limit != 0 ? granted_limit : endpoint->accepted_steps; + granted_limit != 0 ? granted_limit + : endpoint->accepted_step_limit != 0 ? endpoint->accepted_step_limit + : endpoint->accepted_steps; if (used > vertex->continuation_limit) vertex->continuation_limit = used; + /* The granted time quota is monotonic like the step quota: a resume must + * never silently shrink a budget that a previous generation granted. */ + if (endpoint->lookback_limit > vertex->continuation_lookback_limit) + vertex->continuation_lookback_limit = endpoint->lookback_limit; } } +int frame_vertex_continuation_state(const LensVertex *vertex, + GeodesicRayState *state) { + if (vertex == NULL || state == NULL || + vertex->outcome != RAY_OUTCOME_UNRESOLVED || + !isfinite(vertex->continuation_t) || + !isfinite(vertex->continuation_log_alpha_p0) || + !isfinite(vertex->continuation_log_alpha_p0_0) || + !isfinite(vertex->continuation_integration_start_time) || + !isfinite(vertex->continuation_next_step)) + return -1; + for (int axis = 0; axis < 3; ++axis) + if (!isfinite(vertex->continuation_x[axis]) || + !isfinite(vertex->continuation_Pi[axis])) + return -1; + *state = (GeodesicRayState){ + .coordinate_time = vertex->continuation_t, + .x = {vertex->continuation_x[0], vertex->continuation_x[1], + vertex->continuation_x[2]}, + .Pi = {vertex->continuation_Pi[0], vertex->continuation_Pi[1], + vertex->continuation_Pi[2]}, + .log_alpha_p0 = vertex->continuation_log_alpha_p0, + .log_alpha_p0_0 = vertex->continuation_log_alpha_p0_0, + .steps = vertex->continuation_steps, + .integration_start_time = vertex->continuation_integration_start_time, + .next_step = vertex->continuation_next_step, + .rejected_steps = vertex->continuation_rejected_steps, + .rhs_evaluations = vertex->continuation_rhs_evaluations, + .previous_rejected = vertex->continuation_previous_rejected}; + return 0; +} + int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime, const ObserverState *observer, const GeodesicTraceConfig *trace) { @@ -643,14 +710,77 @@ static size_t witness_find(const FrameLensMesh *mesh, size_t a, size_t b) { return SIZE_MAX; } -static unsigned int retry_limit(unsigned int current, - const RefinementConfig *config) { +static unsigned int retry_step_limit(unsigned int current, + const RefinementConfig *config) { const unsigned int remaining = config->max_total_steps - current; return current + (config->retry_step_increment < remaining ? config->retry_step_increment : remaining); } +static double retry_time_limit(double current, + const RefinementConfig *config) { + const double remaining = config->max_total_lookback_time - current; + return current + (config->retry_lookback_increment < remaining + ? config->retry_lookback_increment + : remaining); +} + +/* One local coordinate-time ULP measured with nextafter. It bounds the + * rounding of the boundary-limited final step that produced a lookback stop. */ +static double retry_time_ulp(double t) { + const double up = nextafter(t, INFINITY); + const double down = nextafter(t, -INFINITY); + return fmax(fabs(up - t), fabs(t - down)); +} + +/* Decide whether an UNRESOLVED vertex should be retried and, if so, fill the + * independently saturated step/time budgets. A quota counts as blocking only + * when the ray actually reached it; a retry is refused when any blocking quota + * cannot open a strictly larger representable region, because growing a + * different quota cannot make progress. Returns nonzero when a retry request + * should be emitted. */ +static int retry_budgets(const LensVertex *vertex, + const RefinementConfig *config, + unsigned int *step_limit, double *lookback_limit) { + const unsigned int step_current = vertex->continuation_limit; + const double time_current = vertex->continuation_lookback_limit; + const double start = vertex->continuation_integration_start_time; + /* Use the same quantity and direction as the integrator's stop test: the + * ray is time-blocked when its stop time reached the lookback left boundary + * t <= start - quota. The DP loop exits on exactly that comparison, so a + * single time ULP is allowed only for the boundary-limited final step. */ + const double time_left = start - time_current; + const int time_blocked = + time_current > 0.0 && + vertex->continuation_t <= time_left + retry_time_ulp(time_left); + const int step_blocked = vertex->continuation_steps >= step_current; + const int step_can_grow = + config->retry_step_increment > 0 && step_current < config->max_total_steps; + const int time_can_grow = + config->retry_lookback_increment > 0.0 && + time_current < config->max_total_lookback_time; + const unsigned int step_candidate = + step_can_grow ? retry_step_limit(step_current, config) : step_current; + const double time_candidate = + time_can_grow ? retry_time_limit(time_current, config) : time_current; + /* A candidate only counts when it opens a representable region: integer + * steps must strictly increase, and the time left boundary must strictly + * move. A larger quota that rounds to the same boundary (or is unchanged at + * a large time origin) is not effective growth and must not launch a retry + * that can never progress. */ + const int step_effective = step_candidate > step_current; + const int time_effective = time_candidate > time_current && + (start - time_candidate) < (start - time_current); + if (!(step_blocked || time_blocked)) + return 0; + if ((step_blocked && !step_effective) || (time_blocked && !time_effective)) + return 0; + *step_limit = step_effective ? step_candidate : step_current; + *lookback_limit = time_effective ? time_candidate : time_current; + return 1; +} + static int triangle_allows_children(const FrameLensMesh *mesh, const LensTriangle *triangle, const RefinementConfig *config); @@ -677,7 +807,9 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh, * as is an off-mesh unresolved witness. A witness retry carries its edge so * the refinement decision sees the updated state; both passes share * retry_seen so a promoted witness is never requested twice. */ - if (config->retry_step_increment > 0 && mesh->vertex_count > 0) { + if ((config->retry_step_increment > 0 || + config->retry_lookback_increment > 0.0) && + mesh->vertex_count > 0) { unsigned char *retry_seen = calloc(mesh->vertex_count, 1); if (retry_seen == NULL) return -1; @@ -694,14 +826,14 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh, if (vertex->outcome != RAY_OUTCOME_UNRESOLVED || retry_seen[v]) continue; retry_seen[v] = 1; - if (vertex->continuation_limit >= config->max_total_steps) - continue; /* capped: reported as budget-incomplete, not retried */ - const unsigned int limit = retry_limit(vertex->continuation_limit, config); - if (limit <= vertex->continuation_limit) - continue; + unsigned int step_limit; + double time_limit; + if (!retry_budgets(vertex, config, &step_limit, &time_limit)) + continue; /* blocked quota at cap: reported as budget-incomplete */ FrameSample retry = {.kind = FRAME_SAMPLE_RETRY, .vertex_id = v, - .step_limit = limit, + .step_limit = step_limit, + .lookback_limit = time_limit, .vertex = *vertex}; if (add_sample(mesh, &retry)) { free(retry_seen); @@ -716,15 +848,16 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh, if (retry_seen[v] || vertex->outcome != RAY_OUTCOME_UNRESOLVED) continue; retry_seen[v] = 1; - if (vertex->continuation_limit >= config->max_total_steps) - continue; - const unsigned int limit = retry_limit(vertex->continuation_limit, config); - if (limit <= vertex->continuation_limit) + unsigned int step_limit; + double time_limit; + if (!retry_budgets(vertex, config, &step_limit, &time_limit)) continue; FrameSample retry = {.kind = FRAME_SAMPLE_RETRY, .vertex_id = v, .edge_vertex = {vertex->probe_edge[0], vertex->probe_edge[1]}, - .step_limit = limit, .vertex = *vertex}; + .step_limit = step_limit, + .lookback_limit = time_limit, + .vertex = *vertex}; if (add_sample(mesh, &retry)) { free(retry_seen); return -1; @@ -994,6 +1127,43 @@ void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh, stats->retry_requests = mesh->retry_requests; } +static uint64_t sat_add_u64(uint64_t total, uint64_t value, int *saturated) { + if (UINT64_MAX - total < value) { + *saturated = 1; + return UINT64_MAX; + } + return total + value; +} + +void frame_lens_mesh_trace_stats(const FrameLensMesh *mesh, + FrameTraceStats *stats) { + if (stats == NULL) + return; + *stats = (FrameTraceStats){0}; + if (mesh == NULL) + return; + int saturated = 0; + for (size_t i = 0; i < mesh->vertex_count; ++i) { + const LensVertex *vertex = &mesh->vertices[i]; + if (!vertex->traced) + continue; + stats->accepted_steps = + sat_add_u64(stats->accepted_steps, vertex->trace_accepted_steps, + &saturated); + stats->rejected_steps = + sat_add_u64(stats->rejected_steps, vertex->trace_rejected_steps, + &saturated); + stats->rhs_evaluations = + sat_add_u64(stats->rhs_evaluations, vertex->trace_rhs_evaluations, + &saturated); + ++stats->vertices; + if (vertex->trace_cost_saturated) + ++stats->saturated_vertices; + } + if (saturated && stats->saturated_vertices == 0) + stats->saturated_vertices = 1; +} + static LensVertex midpoint_vertex(const LensVertex *a, const LensVertex *b) { LensVertex result = {.image_x = 0.5 * (a->image_x + b->image_x), .image_y = 0.5 * (a->image_y + b->image_y)}; @@ -1438,6 +1608,18 @@ void frame_retry_config_defaults(RefinementConfig *config, trace->max_steps > (UINT_MAX / 4u) ? trace->max_steps : trace->max_steps * 4u; } + /* The coordinate-time retry budget is a resource quota owned by the + * adaptive stepper; it has no meaning for the fixed-step RK4 path, so an + * RK4 config keeps its zero time fields. */ + if (trace->stepper == GEODESIC_STEPPER_DP54 && + config->retry_lookback_increment == 0.0 && + config->max_total_lookback_time == 0.0) { + config->retry_lookback_increment = trace->max_lookback_time; + config->max_total_lookback_time = + trace->max_lookback_time > DBL_MAX / 4.0 + ? DBL_MAX + : trace->max_lookback_time * 4.0; + } } int frame_lens_mesh_refine(FrameLensMesh *mesh, @@ -1472,20 +1654,24 @@ int frame_lens_mesh_refine_with_progress( if (sample->cached) continue; RayEndpoint endpoint; if (sample->kind == FRAME_SAMPLE_RETRY) { - const LensVertex *v = &sample->vertex; - GeodesicRayState state = { - .coordinate_time = v->continuation_t, - .x = {v->continuation_x[0], v->continuation_x[1], - v->continuation_x[2]}, - .Pi = {v->continuation_Pi[0], v->continuation_Pi[1], - v->continuation_Pi[2]}, - .log_alpha_p0 = v->continuation_log_alpha_p0, - .log_alpha_p0_0 = v->continuation_log_alpha_p0_0, - .steps = v->continuation_steps}; - GeodesicTraceConfig retry_config = *trace; - retry_config.max_steps = sample->step_limit; - endpoint = geodesic_trace_past_from_state(spacetime, &state, - &retry_config); + GeodesicRayState state; + if (frame_vertex_continuation_state(&sample->vertex, &state)) { + /* An UNRESOLVED request without a valid resume payload is a + * protocol error, never a silent discard. */ + endpoint = (RayEndpoint){.magnification = 1.0, + .end_id = SPACETIME_END_NONE, + .outcome = RAY_OUTCOME_INCOMPLETE, + .reason = RAY_REASON_PROTOCOL_ERROR, + .stop_coordinate_time = NAN, + .threshold_value = NAN}; + } else { + GeodesicTraceConfig retry_config = *trace; + retry_config.max_steps = sample->step_limit; + if (sample->lookback_limit > 0.0) + retry_config.max_lookback_time = sample->lookback_limit; + endpoint = geodesic_trace_past_from_state(spacetime, &state, + &retry_config); + } } else { endpoint = geodesic_trace_past(spacetime, observer, sample->vertex.camera_direction, trace); diff --git a/src/frame.h b/src/frame.h index e883329..0bbbb2f 100644 --- a/src/frame.h +++ b/src/frame.h @@ -8,6 +8,7 @@ #include "spacetime.h" #include +#include typedef struct { double image_x, image_y; @@ -29,10 +30,31 @@ typedef struct { double continuation_log_alpha_p0_0; unsigned int continuation_steps; unsigned int continuation_limit; + /* Adaptive (DP54) resume control/cost state, valid with the fields above when + * outcome == RAY_OUTCOME_UNRESOLVED. An integration start time of zero is a + * legal coordinate time; the legacy RK4 path leaves these zero. */ + double continuation_integration_start_time; + double continuation_next_step; + unsigned int continuation_rejected_steps; + unsigned long continuation_rhs_evaluations; + unsigned int continuation_previous_rejected; + /* Granted total coordinate-time lookback budget accumulated for this + * continuation. Zero when the legacy RK4 path supplies no time quota. */ + double continuation_lookback_limit; /* Persistent off-mesh probe witness; also participates in completion checks. * When set, probe_edge holds the sorted edge (a,b) this witness samples. */ int diagnostic_probe; size_t probe_edge[2]; + /* Actual integration cost for this persistent vertex, copied from the + * endpoint for every outcome (not only UNRESOLVED). The endpoint already + * carries cumulative accepted/rejected/RHS counts for a resumed trace, so + * each install overwrites rather than accumulates and a witness promoted in + * place keeps one physical sample counted exactly once. `uint64_t` with the + * saturated flag records a counter that had to be capped instead of wrapping. */ + uint64_t trace_accepted_steps; + uint64_t trace_rejected_steps; + uint64_t trace_rhs_evaluations; + int trace_cost_saturated; } LensVertex; typedef struct { @@ -77,6 +99,13 @@ typedef struct { * budget-incomplete instead of silently blackened. */ unsigned int retry_step_increment; unsigned int max_total_steps; + /* Independent coordinate-time retry budget. The step and time quotas are + * saturated separately and a retry is requested only while a quota that + * actually blocked the ray can still grow. Both zero on a DP trace derive + * from trace->max_lookback_time; a zero increment with a positive cap keeps + * the time budget fixed. Ignored by the legacy RK4 path. */ + double retry_lookback_increment; + double max_total_lookback_time; } RefinementConfig; typedef enum { @@ -91,6 +120,9 @@ typedef struct { size_t edge_vertex[2]; /* For FRAME_SAMPLE_RETRY: the new total accepted-step budget. */ unsigned int step_limit; + /* For FRAME_SAMPLE_RETRY: the new total coordinate-time lookback budget; + * zero keeps the trace config budget. */ + double lookback_limit; int cached; LensVertex vertex; } FrameSample; @@ -116,6 +148,19 @@ typedef struct { size_t budget_incomplete_triangles; } FrameBoundaryStats; +/* Truthful integration cost aggregated over a finalized mesh. Every + * persistent vertex (including promoted probe witnesses) is counted exactly + * once, so summing after refinement does not double-count retries. Counters + * saturate at UINT64_MAX and increment `saturated_vertices` instead of + * wrapping. `vertices` is the number of traced vertices the cost covers. */ +typedef struct { + uint64_t accepted_steps; + uint64_t rejected_steps; + uint64_t rhs_evaluations; + size_t vertices; + size_t saturated_vertices; +} FrameTraceStats; + typedef struct { LensVertex *vertices; LensTriangle *triangles; @@ -188,6 +233,12 @@ const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh, size_t *count); int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id, const RayEndpoint *endpoint); +/* Rebuild the full adaptive resume state recorded in a vertex's continuation + * fields into `state`. Returns 0 on success and -1 when the vertex has no + * valid resume payload. The frame refinement loop and the movie scheduler + * both use this so neither can silently drop a control field. */ +int frame_vertex_continuation_state(const LensVertex *vertex, + GeodesicRayState *state); /* Applies a nonempty completed generation; callers skip frames for which * prepare_generation returned zero. Returns the number of newly added * vertices, zero when no topology change was made, or -1 on failure. */ @@ -209,6 +260,10 @@ int frame_lens_mesh_refine_with_progress( void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh, const RefinementConfig *config, FrameBoundaryStats *stats); +/* Aggregate the per-vertex integration cost of a finalized mesh. Unlike + * boundary_stats this does not modify the mesh and needs no config. */ +void frame_lens_mesh_trace_stats(const FrameLensMesh *mesh, + FrameTraceStats *stats); /* Fill retry defaults (derived from the trace step budget) when the caller * did not configure them explicitly. A zero max_total_steps disables retry * only if retry_step_increment is also zero. */ diff --git a/src/geodesic.c b/src/geodesic.c index 0ec9327..7284fad 100644 --- a/src/geodesic.c +++ b/src/geodesic.c @@ -1,5 +1,6 @@ #include "geodesic.h" #include "asymptotic.h" +#include #include #include @@ -86,23 +87,33 @@ static State add(const State *s, const Derivative *d, double h) { return r; } -static SpacetimePointStatus rk4(const MetricSlab *slab, double t, double h, - State *s) { +/* Classic fixed-step RK4 with explicit RHS accounting. Every rhs() call + * increments *rhs_count before the status is inspected, so a failed stage + * reports the real number of metric evaluations actually performed (for + * example two on a stage-1 failure) instead of a nominal four. The numerical + * operation order is identical to the historical implementation. */ +static SpacetimePointStatus rk4_with_cost(const MetricSlab *slab, double t, + double h, State *s, + unsigned long *rhs_count) { Derivative a, b, c, d; State q; SpacetimePointStatus status = rhs(slab, t, s, &a); + ++*rhs_count; if (status != SPACETIME_POINT_OK) return status; q = add(s, &a, h / 2); status = rhs(slab, t + h / 2, &q, &b); + ++*rhs_count; if (status != SPACETIME_POINT_OK) return status; q = add(s, &b, h / 2); status = rhs(slab, t + h / 2, &q, &c); + ++*rhs_count; if (status != SPACETIME_POINT_OK) return status; q = add(s, &c, h); status = rhs(slab, t + h, &q, &d); + ++*rhs_count; if (status != SPACETIME_POINT_OK) return status; for (int i = 0; i < 3; i++) { @@ -116,6 +127,435 @@ static SpacetimePointStatus rk4(const MetricSlab *slab, double t, double h, return SPACETIME_POINT_OK; } +/* Cost-free wrapper retained for local prototypes that do not track RHS calls. + * The production tracer always uses rk4_with_cost so RK4 reports real cost. */ +static SpacetimePointStatus __attribute__((unused)) +rk4(const MetricSlab *slab, double t, double h, State *s) { + unsigned long unused_count = 0; + return rk4_with_cost(slab, t, h, s, &unused_count); +} + +/* ------------------------------------------------------------------ */ +/* Dormand-Prince 5(4) adaptive stepper */ +/* ------------------------------------------------------------------ */ + +/* Standard Dormand-Prince 5(4)7M embedded pair (Dormand & Prince, J. Comput. + * Appl. Math. 6 (1980) 19-26; Hairer-Norsett-Wanner, Solving ODEs I, 2nd ed., + * Table 5.2). The 5th-order solution is accepted; the embedded 4th-order + * weights supply the error estimate. No FSAL: every trial performs 7 RHS + * evaluations, so a derivative cache never has to cross slab boundaries. */ +static const double DP_C[7] = {0.0, 1.0 / 5.0, 3.0 / 10.0, 4.0 / 5.0, + 8.0 / 9.0, 1.0, 1.0}; +static const double DP_A[7][6] = { + {0.0}, + {1.0 / 5.0}, + {3.0 / 40.0, 9.0 / 40.0}, + {44.0 / 45.0, -56.0 / 15.0, 32.0 / 9.0}, + {19372.0 / 6561.0, -25360.0 / 2187.0, 64448.0 / 6561.0, -212.0 / 729.0}, + {9017.0 / 3168.0, -355.0 / 33.0, 46732.0 / 5247.0, 49.0 / 176.0, + -5103.0 / 18656.0}, + {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0, -2187.0 / 6784.0, + 11.0 / 84.0}}; +static const double DP_B5[7] = {35.0 / 384.0, 0.0, 500.0 / 1113.0, + 125.0 / 192.0, -2187.0 / 6784.0, + 11.0 / 84.0, 0.0}; +static const double DP_B4[7] = {5179.0 / 57600.0, 0.0, + 7571.0 / 16695.0, 393.0 / 640.0, + -92097.0 / 339200.0, 187.0 / 2100.0, + 1.0 / 40.0}; + +/* Quartic continuous extension of the 5th-order Dormand-Prince solution: + * y(theta) = y_before + h * sum_{j=1..4} q_j theta^j, + * q_j = sum_i k_i P[i][j-1], + * for theta in [0, 1]. The column sums of P reproduce the accepted weights + * (y(1) equals the 5th-order candidate), which is why the same accepted state + * drives both the trajectory and the dense event candidates. P is the public + * dense-output matrix of scipy.integrate.RK45 (scipy/integrate/_ivp/rk.py, + * class RK45), the Shampine-Dormand-Prince continuous extension for this + * tableau; the coefficients are quoted verbatim rather than re-derived. The + * estimate is only used to enumerate event candidates, never to replace the + * error-controlled accepted state. */ +static const double DP_DENSE_P[7][4] = { + {1.0, -8048581381.0 / 2820520608.0, 8663915743.0 / 2820520608.0, + -12715105075.0 / 11282082432.0}, + {0.0, 0.0, 0.0, 0.0}, + {0.0, 131558114200.0 / 32700410799.0, -68118460800.0 / 10900136933.0, + 87487479700.0 / 32700410799.0}, + {0.0, -1754552775.0 / 470086768.0, 14199869525.0 / 1410260304.0, + -10690763975.0 / 1880347072.0}, + {0.0, 127303824393.0 / 49829197408.0, -318862633887.0 / 49829197408.0, + 701980252875.0 / 199316789632.0}, + {0.0, -282668133.0 / 205662961.0, 2019193451.0 / 616988883.0, + -1453857185.0 / 822651844.0}, + {0.0, 40617522.0 / 29380423.0, -110615467.0 / 29380423.0, + 69997945.0 / 29380423.0}}; + +static int state_finite(const State *s) { + if (!isfinite(s->coordinate_time) || !isfinite(s->log_alpha_p0) || + !isfinite(s->log_alpha_p0_0)) + return 0; + for (int i = 0; i < 3; ++i) + if (!isfinite(s->x[i]) || !isfinite(s->Pi[i])) + return 0; + return 1; +} + +static int derivative_finite(const Derivative *d) { + if (!isfinite(d->log_alpha_p0)) + return 0; + for (int i = 0; i < 3; ++i) + if (!isfinite(d->x[i]) || !isfinite(d->Pi[i])) + return 0; + return 1; +} + +/* base + h * sum_j w[j] * k[j] for j < n. */ +static State combine_dp(const State *base, const Derivative k[], + const double w[], int n, double h) { + State q = *base; + for (int i = 0; i < 3; ++i) { + double sx = 0.0, spi = 0.0; + for (int j = 0; j < n; ++j) { + sx += w[j] * k[j].x[i]; + spi += w[j] * k[j].Pi[i]; + } + q.x[i] = base->x[i] + h * sx; + q.Pi[i] = base->Pi[i] + h * spi; + } + double sl = 0.0; + for (int j = 0; j < n; ++j) + sl += w[j] * k[j].log_alpha_p0; + q.log_alpha_p0 = base->log_alpha_p0 + h * sl; + return q; +} + +typedef struct { + State cand; /* accepted 5th-order candidate, finite when status is OK */ + Derivative err; /* h * sum((B5 - B4) * k), the embedded error */ + Derivative k1; /* initial derivative for the position error scale */ + Derivative k[7]; /* all seven stage derivatives for the dense output */ +} DpTrial; + +/* One accepted main interval and its quartic dense-output coefficients, i.e. + * the continuous state y(theta) = y_before + h * sum_{j=1..4} q_j theta^j for + * theta in [0, 1] with y(1) the accepted 5th-order candidate. The event layer + * builds these on the stack from the accepted trial; nothing is cached per ray + * and no SoA layout changes. Only x(theta) and log_alpha_p0(theta) are needed + * (the worldtube and threshold polynomials consume their coefficients + * directly), so no separate state evaluator is kept. */ +typedef struct { + State before; + double h; /* signed accepted step (negative in the past-directed trace) */ + Derivative q[4]; +} AcceptedStep; + +static void dense_build(AcceptedStep *dense, const State *before, + const Derivative k[7], double h) { + dense->before = *before; + dense->h = h; + for (int j = 0; j < 4; ++j) { + dense->q[j] = (Derivative){0}; + for (int i = 0; i < 7; ++i) { + const double p = DP_DENSE_P[i][j]; + if (p == 0.0) + continue; + for (int a = 0; a < 3; ++a) { + dense->q[j].x[a] += p * k[i].x[a]; + dense->q[j].Pi[a] += p * k[i].Pi[a]; + } + dense->q[j].log_alpha_p0 += p * k[i].log_alpha_p0; + } + } +} + +typedef enum { + DP_TRIAL_OK = 0, + /* The initial (accepted) state itself could not be evaluated. Shrinking + * the step cannot help, so the caller reports this point status directly. */ + DP_TRIAL_INITIAL_FAILED, + /* A later stage left the known data domain or saw an invalid metric. This + * is retryable with a bounded step reduction. */ + DP_TRIAL_RETRY_STAGE, + /* A later stage hit a data/protocol failure that shrinking cannot fix. */ + DP_TRIAL_FATAL_POINT, + /* Candidate, error or initial derivative was non-finite: reject. */ + DP_TRIAL_NONFINITE +} DpTrialStatus; + +/* One DP5(4) trial from `before` (unchanged) with signed step h. `*rhs` is + * incremented for every actual RHS evaluation, including failed stages. */ +static DpTrialStatus dp_trial(const MetricSlab *slab, const State *before, + double h, DpTrial *trial, + unsigned long *rhs_count, + SpacetimePointStatus *point_status) { + Derivative *k = trial->k; + SpacetimePointStatus status = + rhs(slab, before->coordinate_time, before, &k[0]); + ++*rhs_count; + if (status != SPACETIME_POINT_OK) { + *point_status = status; + return DP_TRIAL_INITIAL_FAILED; + } + for (int i = 1; i < 7; ++i) { + const State q = combine_dp(before, k, DP_A[i], i, h); + status = rhs(slab, before->coordinate_time + DP_C[i] * h, &q, &k[i]); + ++*rhs_count; + if (status != SPACETIME_POINT_OK) { + *point_status = status; + if (status == SPACETIME_POINT_OUT_OF_DOMAIN || + status == SPACETIME_POINT_INVALID_METRIC) + return DP_TRIAL_RETRY_STAGE; + return DP_TRIAL_FATAL_POINT; + } + } + trial->cand = combine_dp(before, k, DP_B5, 7, h); + /* Compute the embedded error directly as h * sum((B5 - B4) * k) instead of + * subtracting the two large solutions, which loses precision when the + * accepted and embedded states nearly coincide. */ + for (int i = 0; i < 3; ++i) { + double sx = 0.0, spi = 0.0; + for (int j = 0; j < 7; ++j) { + const double w = DP_B5[j] - DP_B4[j]; + sx += w * k[j].x[i]; + spi += w * k[j].Pi[i]; + } + trial->err.x[i] = h * sx; + trial->err.Pi[i] = h * spi; + } + { + double sl = 0.0; + for (int j = 0; j < 7; ++j) + sl += (DP_B5[j] - DP_B4[j]) * k[j].log_alpha_p0; + trial->err.log_alpha_p0 = h * sl; + } + trial->k1 = k[0]; + if (!state_finite(&trial->cand) || !derivative_finite(&trial->err) || + !derivative_finite(&trial->k1)) + return DP_TRIAL_NONFINITE; + trial->cand.coordinate_time = before->coordinate_time + h; + trial->cand.steps = before->steps + 1; + trial->cand.log_alpha_p0_0 = before->log_alpha_p0_0; + return DP_TRIAL_OK; +} + +/* Normalized embedded error: the maximum of the component-wise error scaled + * by the agreed position, Pi and L tolerances. Inputs are already finite. */ +static double dp_scaled_error(const State *before, const State *cand, + const Derivative *errv, const Derivative *k1, + double h, const GeodesicTraceConfig *config) { + double worst = 0.0; + for (int i = 0; i < 3; ++i) { + double d = fabs(cand->x[i] - before->x[i]); + const double hd = fabs(h * k1->x[i]); + if (hd > d) + d = hd; + const double scale = config->atol_x + config->rtol * d; + const double e = fabs(errv->x[i]) / scale; + if (e > worst) + worst = e; + } + for (int i = 0; i < 3; ++i) { + const double d = fmax(fabs(before->Pi[i]), fabs(cand->Pi[i])); + const double scale = config->atol_Pi + config->rtol * d; + const double e = fabs(errv->Pi[i]) / scale; + if (e > worst) + worst = e; + } + { + const double dl = fabs(cand->log_alpha_p0 - before->log_alpha_p0); + const double scale = config->atol_L + config->rtol * fmax(1.0, dl); + const double e = fabs(errv->log_alpha_p0) / scale; + if (e > worst) + worst = e; + } + return worst; +} + +/* Controller safety factor and factor clamp [0.2, 5], shared by every + * acceptance decision. */ +static double clamp_factor(double f) { + if (!isfinite(f)) + return 0.2; + if (f < 0.2) + return 0.2; + if (f > 5.0) + return 5.0; + return f; +} + +/* All DP54 tolerances and bounds must be present and valid; they are never + * silently defaulted. */ +static int dp_config_valid(const GeodesicTraceConfig *config) { + return config != NULL && config->coordinate_time_step > 0.0 && + config->max_steps != 0 && isfinite(config->atol_x) && + config->atol_x > 0.0 && isfinite(config->atol_Pi) && + config->atol_Pi > 0.0 && isfinite(config->atol_L) && + config->atol_L > 0.0 && isfinite(config->rtol) && config->rtol > 0.0 && + isfinite(config->min_step) && config->min_step > 0.0 && + isfinite(config->max_step) && config->max_step > 0.0 && + config->min_step <= config->max_step && + config->consecutive_rejection_limit != 0 && + isfinite(config->max_lookback_time) && + config->max_lookback_time > 0.0; +} + +static int stepper_known(GeodesicStepper stepper) { + return stepper == GEODESIC_STEPPER_RK4 || stepper == GEODESIC_STEPPER_DP54; +} + +static RayReason reason_from_point_status(SpacetimePointStatus status); + +/* Largest representable target <= t whose distance from t does not exceed + * `mag` (mag > 0). `t - mag` can round away from t by an ULP; nudging back + * guarantees the accepted interval never exceeds the configured bound. Returns + * t when no representable target would make progress. */ +static double representable_target(double t, double mag) { + double target = t - mag; + if (!(target < t)) + return t; + while (t - target > mag) { + const double up = nextafter(target, t); + if (!(up > target)) + return t; + target = up; + } + return target; +} + +/* Advance exactly one accepted DP54 step from *s, never crossing `left` + * (left < s->coordinate_time). On success the trajectory of *s advances one + * accepted step and the suggested next step/control flags are updated. On + * failure *reason is set and the trajectory of *s is unchanged; `*rhs_total` + * and `*reject_total` always reflect actual RHS/rejection cost, including + * failed stages. Returns 0 on success and -1 on failure. + * + * The accepted interval is defined by a representable target time first; + * stages, the embedded error, the dense step and the committed coordinate time + * then all use that same actual interval. A nominal step whose t + h rounds + * is not silently committed with one time and a different position. */ +static int dp_advance_one(const MetricSlab *slab, + const GeodesicTraceConfig *config, State *s, + double left, RayReason *reason, + unsigned long *rhs_total, + unsigned int *reject_total, + AcceptedStep *dense_out) { + if (!(s->next_step > 0.0) || !isfinite(s->next_step)) + s->next_step = config->coordinate_time_step; + if (!isfinite(s->integration_start_time)) + s->integration_start_time = s->coordinate_time; + double proposal = s->next_step; + if (proposal < config->min_step) + proposal = config->min_step; + if (proposal > config->max_step) + proposal = config->max_step; + const double t0 = s->coordinate_time; + const double remaining = t0 - left; + if (!(remaining > 0.0) || !(proposal > 0.0)) { + *reason = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + const int boundary_limited = remaining < proposal; + double target = boundary_limited ? left : representable_target(t0, proposal); + if (!(target < t0)) { + *reason = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + double actual_h = t0 - target; + /* A boundary-limited final step may legitimately fall below min_step; any + * other sub-floor representable interval is an integration failure. */ + if (!boundary_limited && actual_h < config->min_step) { + *reason = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + unsigned int consecutive = 0; + int rejected_any = 0; + int last_reject_stage = 0; + SpacetimePointStatus last_stage_status = SPACETIME_POINT_OK; + while (1) { + const State before = *s; + const double signed_h = target - before.coordinate_time; + DpTrial trial; + SpacetimePointStatus point_status = SPACETIME_POINT_OK; + const DpTrialStatus status = + dp_trial(slab, &before, signed_h, &trial, rhs_total, &point_status); + if (status == DP_TRIAL_INITIAL_FAILED || status == DP_TRIAL_FATAL_POINT) { + /* Evaluating the accepted state itself failed, or a stage hit a + * data/protocol failure that shrinking cannot repair: report the + * specific point status directly without retrying. */ + *reason = reason_from_point_status(point_status); + return -1; + } + int stage_reject = 0; + double factor = 0.2; + if (status == DP_TRIAL_RETRY_STAGE) { + stage_reject = 1; + last_stage_status = point_status; + } else if (status == DP_TRIAL_NONFINITE) { + /* fall through to the reject path below */ + } else { + const double e = dp_scaled_error(&before, &trial.cand, &trial.err, + &trial.k1, signed_h, config); + if (e <= 1.0) { + /* Accept: commit the candidate and update the controller. A step + * limited only by the left boundary is not representative, so apply at + * most the shrink the error asks for to the original proposal instead + * of letting it permanently depress or artificially grow the + * suggestion. */ + double next_factor = + (e > 0.0) ? clamp_factor(0.9 * pow(e, -0.2)) : 5.0; + if (before.previous_rejected && next_factor > 1.0) + next_factor = 1.0; + double next; + if (boundary_limited && !rejected_any) { + const double f = (next_factor < 1.0) ? next_factor : 1.0; + next = proposal * f; + } else { + next = actual_h * next_factor; + } + if (next < config->min_step) + next = config->min_step; + if (next > config->max_step) + next = config->max_step; + if (dense_out != NULL) + dense_build(dense_out, &before, trial.k, signed_h); + *s = trial.cand; + /* The committed time is exactly the representable target the interval + * was built from, independent of the stage-time rounding. */ + s->coordinate_time = target; + s->previous_rejected = 0; + s->next_step = next; + return 0; + } + factor = (e > 0.0) ? clamp_factor(0.9 * pow(e, -0.2)) : 0.2; + } + /* Rejected trial: never commit trajectory, time or counters as accepted. */ + ++*reject_total; + rejected_any = 1; + s->previous_rejected = 1; + last_reject_stage = stage_reject; + if (++consecutive >= config->consecutive_rejection_limit) { + *reason = last_reject_stage ? reason_from_point_status(last_stage_status) + : RAY_REASON_INTEGRATION_ERROR; + return -1; + } + if (factor > 1.0) + factor = 1.0; + double shrunk = actual_h * factor; + if (shrunk > remaining) + shrunk = remaining; + target = (shrunk >= remaining) ? left : representable_target(t0, shrunk); + if (!(target < t0)) { + *reason = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + actual_h = t0 - target; + if (actual_h < config->min_step) { + *reason = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + } +} + int geodesic_initialize_past_ray_metric(const MetricData *metric, const ObserverState *o, const double n[3], State *s) { @@ -140,6 +580,13 @@ int geodesic_initialize_past_ray_metric(const MetricData *metric, s->log_alpha_p0_0 = s->log_alpha_p0; s->coordinate_time = o->coordinate_time; s->steps = 0; + /* Adaptive window starts at the activation/entry state; next_step is left + * unset (0) so the first DP54 advance derives it from the config. */ + s->integration_start_time = s->coordinate_time; + s->next_step = 0.0; + s->rejected_steps = 0; + s->rhs_evaluations = 0; + s->previous_rejected = 0; return isfinite(s->log_alpha_p0) ? 0 : -1; } @@ -203,12 +650,13 @@ static AsymLifecycleMode asym_lifecycle_mode(const SpacetimeSource *source) { static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab, SpacetimeEndId end_id, const State *before, - double h, State *after) { + double h, State *after, + unsigned long *rhs_total) { double f_lo = 0.0, f_hi = 1.0; for (int iteration = 0; iteration < 64; ++iteration) { const double f = 0.5 * (f_lo + f_hi); State mid = *before; - if (rk4(slab, before->coordinate_time, h * f, &mid)) + if (rk4_with_cost(slab, before->coordinate_time, h * f, &mid, rhs_total)) return ASYMPTOTIC_INVALID; mid.coordinate_time = before->coordinate_time + h * f; double value; @@ -222,13 +670,585 @@ static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab, f_lo = f; } *after = *before; - if (rk4(slab, before->coordinate_time, h * f_hi, after)) + if (rk4_with_cost(slab, before->coordinate_time, h * f_hi, after, rhs_total)) return ASYMPTOTIC_INVALID; after->coordinate_time = before->coordinate_time + h * f_hi; after->steps = before->steps + 1; return ASYMPTOTIC_OK; } +/* One local coordinate-time ULP at t, measured with nextafter (there is no + * portable ULP function). This is the true resolution at which coordinate + * times near t can be distinguished, independent of any relative tolerance. */ +static double time_ulp(double t) { + const double up = nextafter(t, INFINITY); + const double down = nextafter(t, -INFINITY); + return fmax(fabs(up - t), fabs(t - down)); +} + +/* Integrate from `before` to the representable `t_target` (< before.t) with + * the DP54 controller only. Classification-free: no threshold, escape or + * nested event handling, and no recursion into the advance loop. A non-zero + * representable interval is always actually integrated; the earlier + * "copy the state and rewrite only the time" shortcut is gone because it put + * the endpoint off the trajectory. Only `t_target == before.t` may copy + * (with no time change). On failure *reason_out carries the specific + * metric/integration reason so the caller can report it. The working copy's + * accepted-step count is irrelevant to the main trajectory; its RHS/rejection + * cost is accumulated into the shared counters. Returns 0 on success. */ +static int dp_subintegrate(const MetricSlab *slab, + const GeodesicTraceConfig *config, + const State *before, double t_target, State *out, + RayReason *reason_out, unsigned long *rhs_total, + unsigned int *reject_total) { + if (t_target == before->coordinate_time) { + *out = *before; + return 0; + } + if (!(t_target < before->coordinate_time)) { + *reason_out = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + State work = *before; + if (!(work.next_step > 0.0) || !isfinite(work.next_step)) + work.next_step = config->coordinate_time_step; + const double snap = 2.0 * time_ulp(t_target); + while (work.coordinate_time > t_target) { + RayReason reason = RAY_REASON_INTEGRATION_ERROR; + if (dp_advance_one(slab, config, &work, t_target, &reason, rhs_total, + reject_total, NULL)) { + *reason_out = reason; + return -1; + } + /* The accepted boundary step lands at the representable target up to + * rounding. Snap only when it is already within a couple of local ULPs; + * a larger gap must keep integrating (or fail), never fabricate a + * position. */ + if (work.coordinate_time > t_target && + work.coordinate_time - t_target <= snap) + work.coordinate_time = t_target; + } + if (fabs(work.coordinate_time - t_target) > snap) { + *reason_out = RAY_REASON_INTEGRATION_ERROR; + return -1; + } + work.coordinate_time = t_target; + *out = work; + return 0; +} + +/* DP54 counterpart of localize_worldtube_crossing: bounded root localization + * on one accepted main step using the same error-controlled DP54 subinterval + * integration (never RK4, which would mix stepper policies). `after` lands on + * the outside end of the bracket and carries a single accepted main step. + * + * The bracket stop uses the local step magnitude and the real coordinate-time + * ULP, so a distant time origin (large |t| with a small step) cannot widen the + * bracket or short-circuit the integration. On a metric-integration failure + * the specific reason is returned through *integration_reason; a worldtube + * descriptor failure stays ASYMPTOTIC_INVALID/TIME_RANGE_EXHAUSTED and leaves + * *integration_reason at the protocol default. + * + * `f_lo` / `f_hi` seed the bracket. The dense polynomial supplies a bracket + * that is already known to be inside/outside, which keeps a first exit from + * being confused with a later re-entry; passing 0 and 1 reproduces the + * whole-step search. */ +static AsymptoticStatus dp_localize_worldtube_crossing( + const MetricSlab *slab, const GeodesicTraceConfig *config, + SpacetimeEndId end_id, const State *before, double h, double f_lo, + double f_hi, State *after, RayReason *integration_reason, + unsigned long *rhs_total, unsigned int *reject_total) { + const double t0 = before->coordinate_time; + const double t_end = t0 + h; /* actual accepted endpoint time */ + const double span = t_end - t0; + const double stop_tol = + fmax(1e-12 * fabs(span), 2.0 * time_ulp(t0)); + *integration_reason = RAY_REASON_PROTOCOL_ERROR; + for (int iteration = 0; iteration < 256; ++iteration) { + if (fabs(span) * (f_hi - f_lo) <= stop_tol) + break; + const double f = 0.5 * (f_lo + f_hi); + const double target = t0 + f * span; + State mid; + if (dp_subintegrate(slab, config, before, target, &mid, + integration_reason, rhs_total, reject_total)) + return ASYMPTOTIC_INVALID; + double value; + const int status = asymptotic_worldtube_value( + slab->source, end_id, mid.coordinate_time, mid.x, &value); + if (status != ASYMPTOTIC_OK) + return (AsymptoticStatus)status; + if (value >= 0.0) + f_hi = f; + else + f_lo = f; + } + const double target_hi = (f_hi >= 1.0) ? t_end : t0 + f_hi * span; + if (dp_subintegrate(slab, config, before, target_hi, after, + integration_reason, rhs_total, reject_total)) + return ASYMPTOTIC_INVALID; + /* The subintegration cost is real, but only the main step counts as an + * accepted main step. */ + after->steps = before->steps + 1; + return ASYMPTOTIC_OK; +} + +/* Restore the last accepted trajectory while preserving the accumulated + * adaptive cost/control fields, so a failed event resolution leaves the + * caller state and the endpoint's last trusted state consistent. */ +static void restore_accepted_trajectory(State *s, const State *before) { + const unsigned long rhs = s->rhs_evaluations; + const unsigned int rejected = s->rejected_steps; + const unsigned int prev = s->previous_rejected; + const double next = s->next_step; + const double start = s->integration_start_time; + *s = *before; + s->rhs_evaluations = rhs; + s->rejected_steps = rejected; + s->previous_rejected = prev; + s->next_step = next; + s->integration_start_time = start; +} + +/* ------------------------------------------------------------------ */ +/* Dense-output event layer */ +/* */ +/* The accepted DP54 step carries a quartic continuous extension. It */ +/* is used only to enumerate event candidates for the error-controlled */ +/* trajectory; every candidate is re-localized with the same DP54 */ +/* subintegration before it can terminate the ray. */ +/* ------------------------------------------------------------------ */ + +static double monitored_threshold_value(const MetricSlab *slab, + ThresholdKind kind, const State *s); +#ifdef GEODESIC_EVENT_TESTING +int geodesic_event_polynomial_exit(const double *coeff, int degree, + double *theta, double *lo, double *hi); +int geodesic_event_worldtube_dense_probe(const MetricSlab *slab, + const GeodesicTraceConfig *config, + SpacetimeEndId end_id, State *state, + double left, double theta, + double *dense_value, + double *actual_value); +#endif + +#define EVENT_POLY_MAX_DEGREE 8 +#define EVENT_POLY_MAX_ROOTS 8 + +static double poly_eval(const double *c, int degree, double x) { + double value = 0.0; + for (int k = degree; k >= 0; --k) + value = value * x + c[k]; + return value; +} + +/* All distinct real roots of c[0..degree] in [0,1], ascending. Standard + * recursive derivative root isolation: the derivative roots partition [0,1] + * into monotone intervals of the polynomial, each of which carries at most one + * root, found by bisection on a sign change. Critical points where the + * polynomial itself vanishes are added explicitly so that tangencies and + * higher-multiplicity roots are not lost. The zero tolerance is scaled by the + * local coefficient magnitude, so it tracks the achievable conditioning of the + * polynomial instead of a fixed iteration count. */ +static int poly_roots_unit(const double *c, int degree, double *roots) { + if (degree <= 0) + return 0; + double scale = 0.0; + for (int k = 0; k <= degree; ++k) + scale += fabs(c[k]); + if (!(scale > 0.0) || !isfinite(scale)) + return 0; + const double ztol = 128.0 * DBL_EPSILON * scale; + if (degree == 1) { + if (c[1] == 0.0) + return 0; + const double r = -c[0] / c[1]; + if (r >= 0.0 && r <= 1.0) { + roots[0] = r; + return 1; + } + return 0; + } + double d[EVENT_POLY_MAX_DEGREE]; + for (int k = 0; k < degree; ++k) + d[k] = (double)(k + 1) * c[k + 1]; + double crit[EVENT_POLY_MAX_ROOTS]; + const int nc = poly_roots_unit(d, degree - 1, crit); + + double nodes[EVENT_POLY_MAX_ROOTS + 2]; + int nn = 0; + nodes[nn++] = 0.0; + for (int i = 0; i < nc; ++i) + if (crit[i] > 0.0 && crit[i] < 1.0) + nodes[nn++] = crit[i]; + nodes[nn++] = 1.0; + int m = 0; + for (int i = 0; i < nn; ++i) + if (m == 0 || nodes[i] > nodes[m - 1]) + nodes[m++] = nodes[i]; + + double found[EVENT_POLY_MAX_ROOTS + 2]; + int count = 0; + for (int i = 0; i < m; ++i) + if (fabs(poly_eval(c, degree, nodes[i])) <= ztol) + found[count++] = nodes[i]; + for (int i = 0; i + 1 < m; ++i) { + const double pa = poly_eval(c, degree, nodes[i]); + const double pb = poly_eval(c, degree, nodes[i + 1]); + if ((pa < -ztol && pb > ztol) || (pa > ztol && pb < -ztol)) { + double a = nodes[i], b = nodes[i + 1], fa = pa; + for (int it = 0; it < 256; ++it) { + const double mid = 0.5 * (a + b); + if (b - a <= 8.0 * DBL_EPSILON * fmax(1.0, fabs(a))) + break; + const double fm = poly_eval(c, degree, mid); + if (fabs(fm) <= ztol) { + a = b = mid; + break; + } + if ((fa < 0.0) == (fm < 0.0)) { + a = mid; + fa = fm; + } else { + b = mid; + } + } + found[count++] = 0.5 * (a + b); + } + } + for (int i = 0; i < count; ++i) + for (int j = i + 1; j < count; ++j) + if (found[j] < found[i]) { + const double t = found[i]; + found[i] = found[j]; + found[j] = t; + } + int r = 0; + for (int i = 0; i < count; ++i) { + if (r > 0 && found[i] - roots[r - 1] <= 16.0 * DBL_EPSILON) + continue; + if (r >= EVENT_POLY_MAX_ROOTS) + break; + roots[r++] = found[i]; + } + return r; +} + +typedef struct { + int has; /* an inside->outside crossing candidate was found */ + double theta; /* root estimate in [0,1] */ + double lo, hi; /* monotone bracket with value(lo)<=0 and value(hi)>0 */ +} EventCrossing; + +/* Earliest inside->outside crossing of the polynomial c in [0,1], where the + * negative side is "inside" (F < 0). A tangent touch (even multiplicity) and + * an outside->inside entry are not crossings. A root exactly at theta == 1 + * is not returned: a single touch at the step endpoint is re-checked on the + * next step as a boundary start. The bracket is chosen between the + * neighbouring root-free sign samples so an earlier tangency cannot be + * mistaken for the exit during re-localization. */ +static EventCrossing poly_earliest_exit(const double *c, int degree) { + EventCrossing result = {0}; + double roots[EVENT_POLY_MAX_ROOTS]; + const int nr = poly_roots_unit(c, degree, roots); + double scale = 0.0; + for (int k = 0; k <= degree; ++k) + scale += fabs(c[k]); + if (!(scale > 0.0)) + return result; + const double ztol = 128.0 * DBL_EPSILON * scale; + for (int i = 0; i < nr; ++i) { + const double r = roots[i]; + if (r >= 1.0) + continue; + double xl, xr, fl, fr; + if (i == 0) { + xl = 0.0; + fl = poly_eval(c, degree, 0.0); + } else { + xl = 0.5 * (roots[i - 1] + r); + fl = poly_eval(c, degree, xl); + } + xr = (i + 1 < nr) ? 0.5 * (r + roots[i + 1]) : 0.5 * (r + 1.0); + fr = poly_eval(c, degree, xr); + if (fl <= 0.0 && fr > ztol) { + result.has = 1; + result.theta = r; + result.lo = (i == 0) ? 0.0 : xl; + result.hi = xr; + return result; + } + } + return result; +} + +/* Start of the last monotone segment (largest derivative root below theta == 1) + * of the polynomial c, or 0 when there is no interior critical point. Used to + * build a closed-end threshold candidate whose only crossing is at theta == 1. */ +static double poly_last_segment_start(const double *c, int degree) { + if (degree <= 1) + return 0.0; + double d[EVENT_POLY_MAX_DEGREE]; + for (int k = 0; k < degree; ++k) + d[k] = (double)(k + 1) * c[k + 1]; + double crit[EVENT_POLY_MAX_ROOTS]; + const int nc = poly_roots_unit(d, degree - 1, crit); + double lo = 0.0; + for (int i = 0; i < nc; ++i) + if (crit[i] > lo && crit[i] < 1.0) + lo = crit[i]; + return lo; +} + +#ifdef GEODESIC_EVENT_TESTING +/* Internal hook for the focused event regression (declared by the test, not + * part of the renderer API). Reports the earliest inside->outside crossing of + * a polynomial given its coefficients in increasing power order. */ +int geodesic_event_polynomial_exit(const double *coeff, int degree, + double *theta, double *lo, double *hi) { + const EventCrossing crossing = poly_earliest_exit(coeff, degree); + if (!crossing.has) + return 0; + if (theta != NULL) + *theta = crossing.theta; + if (lo != NULL) + *lo = crossing.lo; + if (hi != NULL) + *hi = crossing.hi; + return 1; +} +#endif /* GEODESIC_EVENT_TESTING */ + +/* F(theta) = |x(theta) - center(t(theta))|^2 - radius(t(theta))^2 for one + * declared end over an accepted dense step. Only valid when the worldtube + * sample is present and its velocity/radius_rate are constant through the + * segment, which is what makes F a polynomial (degree <= 8). Returns 1 with + * `coeff` filled, 0 when there is no usable sample, and -1 when the end uses + * an arbitrary accelerated worldtube that this layer does not support. */ +static int worldtube_dense_polynomial(const SpacetimeSource *source, + SpacetimeEndId end_id, + const AcceptedStep *dense, + double coeff[EVENT_POLY_MAX_DEGREE + 1]) { + SpacetimeEscapeWorldtubeSample sample; + /* Sample strictly inside the accepted interval: the step has been clamped to + * a motion-segment boundary, so an interior sample belongs to one segment + * even when the step starts exactly on a boundary. At a large time origin + * t + h/2 can round onto a step endpoint, so use the representable interior + * time (or the past-side endpoint if the step is a single ULP wide) and back + * out the sample offset from the real times, never from a nominal h/2. */ + const double t_before = dense->before.coordinate_time; + double t_mid = t_before + 0.5 * dense->h; + if (t_mid == t_before) { + const double past = nextafter(t_before, -INFINITY); + const double after_t = t_before + dense->h; + t_mid = (past > after_t) ? past : after_t; + } + if (spacetime_escape_worldtube_sample(source, end_id, t_mid, &sample)) + return 0; + if (!sample.valid) + return 0; + if (!sample.velocity_constant) + return -1; + const double h = dense->h; + /* The sample is the reference; shift the linear center/radius back to the + * step start using the actual representable sample offset. */ + const double sample_dt = t_mid - t_before; + double d0[3], uc[5][3]; + for (int i = 0; i < 3; ++i) + d0[i] = dense->before.x[i] - sample.center[i] + sample_dt * sample.velocity[i]; + for (int k = 1; k <= 4; ++k) + for (int i = 0; i < 3; ++i) + uc[k][i] = dense->q[k - 1].x[i]; + for (int i = 0; i < 3; ++i) + uc[1][i] -= sample.velocity[i]; + const double g = sample.radius_rate * h; + const double radius0 = sample.radius - sample_dt * sample.radius_rate; + for (int k = 0; k <= EVENT_POLY_MAX_DEGREE; ++k) + coeff[k] = 0.0; + coeff[0] = dot(d0, d0) - radius0 * radius0; + for (int k = 1; k <= 4; ++k) + coeff[k] += 2.0 * h * dot(d0, uc[k]); + coeff[1] -= 2.0 * radius0 * g; + coeff[2] -= g * g; + for (int a = 1; a <= 4; ++a) + for (int b = 1; b <= 4; ++b) + coeff[a + b] += h * h * dot(uc[a], uc[b]); + return 1; +} + +#ifdef GEODESIC_EVENT_TESTING +/* Internal hook for the focused event regression (declared by the test, not + * part of the renderer API). Runs one production DP advance from *state, then + * evaluates the worldtube dense polynomial and the actual worldtube F at the + * same theta, so a large time origin can be checked for half-step rounding + * drift between the polynomial and the error-controlled trajectory. */ +int geodesic_event_worldtube_dense_probe(const MetricSlab *slab, + const GeodesicTraceConfig *config, + SpacetimeEndId end_id, State *state, + double left, double theta, + double *dense_value, + double *actual_value) { + if (slab == NULL || config == NULL || state == NULL || dense_value == NULL || + actual_value == NULL || theta < 0.0 || theta > 1.0) + return 0; + RayReason reason = RAY_REASON_INTEGRATION_ERROR; + unsigned long rhs = state->rhs_evaluations; + unsigned int rej = state->rejected_steps; + AcceptedStep dense; + if (dp_advance_one(slab, config, state, left, &reason, &rhs, &rej, &dense)) + return 0; + state->rhs_evaluations = rhs; + state->rejected_steps = rej; + double coeff[EVENT_POLY_MAX_DEGREE + 1]; + if (worldtube_dense_polynomial(slab->source, end_id, &dense, coeff) != 1) + return 0; + *dense_value = poly_eval(coeff, EVENT_POLY_MAX_DEGREE, theta); + State at; + if (dp_subintegrate(slab, config, &dense.before, + dense.before.coordinate_time + dense.h * theta, &at, + &reason, &rhs, &rej)) + return 0; + state->rhs_evaluations = rhs; + state->rejected_steps = rej; + double actual = 0.0; + if (asymptotic_worldtube_value(slab->source, end_id, at.coordinate_time, at.x, + &actual) != ASYMPTOTIC_OK) + return 0; + *actual_value = actual; + return 1; +} +#endif /* GEODESIC_EVENT_TESTING */ + +/* Monitored dark-threshold polynomial for the two L-based policies. The + * result is (monitored value) - threshold, so a <0 -> >=0 crossing is a + * dark event. LOG_P0 subtracts a metric function of position and is not a + * polynomial; it is deliberately left to the trusted-state check to keep its + * established semantics. */ +static int threshold_dense_polynomial(const GeodesicTraceConfig *config, + const AcceptedStep *dense, + double coeff[5]) { + const ThresholdKind kind = config->threshold.kind; + if (kind != THRESHOLD_LOG_ALPHA_P0 && kind != THRESHOLD_LOG_ENERGY_GROWTH) + return 0; + const double base = + (kind == THRESHOLD_LOG_ENERGY_GROWTH) + ? dense->before.log_alpha_p0 - dense->before.log_alpha_p0_0 + : dense->before.log_alpha_p0; + coeff[0] = base - config->threshold.value; + for (int j = 0; j < 4; ++j) + coeff[j + 1] = dense->h * dense->q[j].log_alpha_p0; + return 1; +} + +/* Localize the first monitored-value crossing inside a bracket [f_lo, f_hi] + * of one accepted step (theta parameters, h the signed step). The bracket must + * be confirmed on the actual error-controlled trajectory: monitored(f_lo) < T + * and monitored(f_hi) >= T. Bisection then stays inside that local interval, + * so a later crossing of a non-monotonic L pulse cannot be picked up instead. + * Returns 1 with *stop, 0 if the actual bracket is not consistent, -1 on + * integration failure. */ +static int dp_localize_threshold_bracket(const MetricSlab *slab, + const GeodesicTraceConfig *config, + const State *before, double h, + double f_lo, double f_hi, + State *stop, RayReason *reason, + unsigned long *rhs_total, + unsigned int *reject_total) { + const double t0 = before->coordinate_time; + const double span = h; + if (!(span < 0.0) || !(f_hi > f_lo)) + return 0; + State lo_state, hi_state; + if (dp_subintegrate(slab, config, before, t0 + span * f_lo, &lo_state, reason, + rhs_total, reject_total) || + dp_subintegrate(slab, config, before, t0 + span * f_hi, &hi_state, reason, + rhs_total, reject_total)) + return -1; + const double lo_value = + monitored_threshold_value(slab, config->threshold.kind, &lo_state); + const double hi_value = + monitored_threshold_value(slab, config->threshold.kind, &hi_state); + if (!(isfinite(lo_value) && lo_value < config->threshold.value) || + !(isfinite(hi_value) && hi_value >= config->threshold.value)) + return 0; + const double stop_tol = + fmax(1e-12 * fabs(span), 2.0 * time_ulp(t0)); + for (int it = 0; it < 256 && fabs(span) * (f_hi - f_lo) > stop_tol; ++it) { + const double f = 0.5 * (f_lo + f_hi); + State mid; + if (dp_subintegrate(slab, config, before, t0 + span * f, &mid, reason, + rhs_total, reject_total)) + return -1; + const double value = + monitored_threshold_value(slab, config->threshold.kind, &mid); + if (isfinite(value) && value >= config->threshold.value) { + f_hi = f; + hi_state = mid; + } else { + f_lo = f; + } + } + *stop = hi_state; + return 1; +} + +/* Per-step worldtube scan: verify that every declared end can describe the + * current state, reject arbitrary accelerated worldtubes explicitly, and clamp + * the step so it cannot cross a motion-segment boundary (which would break the + * constant-velocity polynomial used by the dense event layer). On success + * *event_left is the tightened left bound; on TIME_RANGE_EXHAUSTED + * *time_range_end carries the end that exhausted its history. */ +static AsymptoticStatus worldtube_prestep_scan( + const SpacetimeSource *source, size_t end_count, double t, double step_left, + double *event_left, SpacetimeEndId *time_range_end) { + *event_left = step_left; + for (size_t i = 0; i < end_count; ++i) { + SpacetimeAsymptoticEnd end; + if (spacetime_asymptotic_end(source, i, &end)) + return ASYMPTOTIC_INVALID; + SpacetimeEscapeWorldtubeSample sample; + if (spacetime_escape_worldtube_sample(source, end.end_id, t, &sample)) + return ASYMPTOTIC_INVALID; + if (!sample.valid) { + *time_range_end = end.end_id; + return ASYMPTOTIC_TIME_RANGE_EXHAUSTED; + } + if (!(sample.radius > 0.0) || !isfinite(sample.radius) || + !isfinite(sample.radius_rate)) + return ASYMPTOTIC_INVALID; + if (!sample.velocity_constant) + return ASYMPTOTIC_UNSUPPORTED; + const double boundary = + spacetime_escape_worldtube_next_segment(source, end.end_id, t); + if (isfinite(boundary) && boundary > *event_left) + *event_left = boundary; + } + return ASYMPTOTIC_OK; +} + +/* A dense event candidate could not be confirmed on the error-controlled + * trajectory. Restore the accepted step (the main trial is not consumed), + * count one rejected trial and halve the suggested step. Returns nonzero when + * the consecutive-rejection limit is reached or the step cannot shrink below + * the configured floor, which the caller reports as an integration failure. */ +static int event_retry_limit(State *s, const GeodesicTraceConfig *config, + const State *before, unsigned int *consecutive, + unsigned int *reject_total) { + restore_accepted_trajectory(s, before); + ++*reject_total; + s->rejected_steps = *reject_total; + s->previous_rejected = 1; + ++*consecutive; + const double halved = (s->next_step > 0.0) + ? 0.5 * s->next_step + : 0.5 * config->coordinate_time_step; + if (*consecutive >= config->consecutive_rejection_limit || + !(halved > config->min_step)) + return 1; + s->next_step = halved; + return 0; +} + static RayReason reason_from_point_status(SpacetimePointStatus status) { switch (status) { case SPACETIME_POINT_TIME_UNAVAILABLE: @@ -254,6 +1274,11 @@ static void record_final_state(RayEndpoint *out, const State *s) { } out->final_log_alpha_p0 = NAN; out->final_log_alpha_p0_0 = NAN; + out->integration_start_time = NAN; + out->next_step = NAN; + out->rejected_steps = 0; + out->rhs_evaluations = 0; + out->previous_rejected = 0; return; } out->stop_coordinate_time = s->coordinate_time; @@ -264,6 +1289,11 @@ static void record_final_state(RayEndpoint *out, const State *s) { } out->final_log_alpha_p0 = s->log_alpha_p0; out->final_log_alpha_p0_0 = s->log_alpha_p0_0; + out->integration_start_time = s->integration_start_time; + out->next_step = s->next_step; + out->rejected_steps = s->rejected_steps; + out->rhs_evaluations = s->rhs_evaluations; + out->previous_rejected = s->previous_rejected; } static void set_incomplete(RayEndpoint *out, RayReason reason, @@ -332,8 +1362,7 @@ static GeodesicAdvanceResult legacy_escape(const MetricSlab *slab, GeodesicAdvanceResult geodesic_advance_past_ray( const MetricSlab *slab, State *s, double slab_left_time, const GeodesicTraceConfig *config, RayEndpoint *out) { - if (!slab || !s || !config || !out || config->coordinate_time_step <= 0 || - !config->max_steps || !isfinite(slab_left_time) || + if (!slab || !s || !config || !out || !isfinite(slab_left_time) || slab_left_time > s->coordinate_time) { if (out != NULL) { out->outcome = RAY_OUTCOME_INCOMPLETE; @@ -343,6 +1372,27 @@ GeodesicAdvanceResult geodesic_advance_past_ray( } return GEODESIC_ADVANCE_FAILED; } + /* An unknown stepper code must be rejected explicitly, exactly like an + * unknown wire code; "not DP54" must never fall through to the RK4 path. */ + if (!stepper_known(config->stepper)) { + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; + /* Record the resource quotas the caller granted this trace regardless of how + * it terminates, so the retry layer can accumulate the step and time budgets + * independently of the spent counts. */ + out->lookback_limit = use_dp ? config->max_lookback_time : 0.0; + out->accepted_step_limit = config->max_steps; + if (use_dp) { + if (!dp_config_valid(config)) { + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + } else if (config->coordinate_time_step <= 0 || !config->max_steps) { + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } const AsymLifecycleMode mode = asym_lifecycle_mode(slab->source); if (mode == ASYM_LIFECYCLE_PROTOCOL_ERROR) { set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); @@ -351,7 +1401,22 @@ GeodesicAdvanceResult geodesic_advance_past_ray( const int directed = mode == ASYM_LIFECYCLE_READY; const size_t end_count = directed ? spacetime_asymptotic_end_count(slab->source) : 0; - while (s->coordinate_time > slab_left_time) { + /* Effective left bound: for DP54 the explicit lookback budget is an + * additional, independent limit on top of the slab boundary. */ + double step_left = slab_left_time; + double lookback_left = -INFINITY; + unsigned long rhs_total = s->rhs_evaluations; + unsigned int reject_total = s->rejected_steps; + if (use_dp) { + if (!(s->next_step > 0.0) || !isfinite(s->next_step)) + s->next_step = config->coordinate_time_step; + if (!isfinite(s->integration_start_time)) + s->integration_start_time = s->coordinate_time; + lookback_left = s->integration_start_time - config->max_lookback_time; + if (lookback_left > step_left) + step_left = lookback_left; + } + while (s->coordinate_time > step_left) { /* The threshold is checked only on trusted initial/accepted states. A * trial stage that crosses it does not by itself produce DARK. */ if (threshold_reached(slab, config, s, out)) @@ -369,77 +1434,479 @@ GeodesicAdvanceResult geodesic_advance_past_ray( out->threshold_value = NAN; return GEODESIC_ADVANCE_TERMINATED; } - const double h = -fmin(config->coordinate_time_step, - s->coordinate_time - slab_left_time); - const State before = *s; - const SpacetimePointStatus step_status = rk4(slab, s->coordinate_time, h, s); - if (step_status != SPACETIME_POINT_OK) { - set_incomplete(out, reason_from_point_status(step_status), &before); - return GEODESIC_ADVANCE_FAILED; - } - s->coordinate_time += h; - ++s->steps; - if (!directed) - continue; - for (size_t i = 0; i < end_count; ++i) { - SpacetimeAsymptoticEnd end; - if (spacetime_asymptotic_end(slab->source, i, &end)) { - set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before); + /* Clamp the step to the next motion-segment boundary and reject an + * arbitrary accelerated worldtube explicitly before any integration; the + * dense event polynomial is only valid on a constant-velocity segment. */ + double event_left = step_left; + if (directed) { + SpacetimeEndId tr_end = SPACETIME_END_NONE; + const AsymptoticStatus scan = worldtube_prestep_scan( + slab->source, end_count, s->coordinate_time, step_left, &event_left, + &tr_end); + if (scan == ASYMPTOTIC_UNSUPPORTED) { + set_incomplete(out, RAY_REASON_UNSUPPORTED, s); return GEODESIC_ADVANCE_FAILED; } - double f_before, f_after; - const int before_status = asymptotic_worldtube_value( - slab->source, end.end_id, before.coordinate_time, before.x, - &f_before); - const int after_status = asymptotic_worldtube_value( - slab->source, end.end_id, s->coordinate_time, s->x, &f_after); - if (before_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED || - after_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { - /* A first-class terminal reason, matching pre-route exhaustion: - * preserve the end id and install it as terminated provenance. */ - set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before); - out->end_id = end.end_id; + if (scan == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = tr_end; return GEODESIC_ADVANCE_TERMINATED; } - if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) { - /* The backend cannot describe its own worldtube; this is an explicit - * failure, not a physical escape. */ - set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before); + if (scan != ASYMPTOTIC_OK) { + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); return GEODESIC_ADVANCE_FAILED; } - /* Strict inside->outside: the step must end strictly outside, so a - * single touch at F == 0 (a tangent) is not accepted as a crossing. - * A crossing whose root lands exactly on a step boundary is picked up - * on the following step as f_before == 0, f_after > 0. */ - if (f_before > 0.0 || f_after <= 0.0) + } + + if (!use_dp) { + const State before = *s; + const double h = + -fmin(config->coordinate_time_step, s->coordinate_time - event_left); + const SpacetimePointStatus step_status = + rk4_with_cost(slab, s->coordinate_time, h, s, &rhs_total); + /* The failed step leaves the trajectory untouched; only the real RHS + * cost is committed. RK4 never rejects, so rejected_steps stays put. */ + s->rhs_evaluations = rhs_total; + if (step_status != SPACETIME_POINT_OK) { + set_incomplete(out, reason_from_point_status(step_status), s); + return GEODESIC_ADVANCE_FAILED; + } + s->coordinate_time += h; + ++s->steps; + if (!directed) continue; - State crossing; - const AsymptoticStatus localized = localize_worldtube_crossing( - slab, end.end_id, &before, h, &crossing); - if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { - set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before); - out->end_id = end.end_id; - return GEODESIC_ADVANCE_TERMINATED; - } - if (localized != ASYMPTOTIC_OK) { - set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before); + for (size_t i = 0; i < end_count; ++i) { + SpacetimeAsymptoticEnd end; + if (spacetime_asymptotic_end(slab->source, i, &end)) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + double f_before, f_after; + const int before_status = asymptotic_worldtube_value( + slab->source, end.end_id, before.coordinate_time, before.x, + &f_before); + const int after_status = asymptotic_worldtube_value( + slab->source, end.end_id, s->coordinate_time, s->x, &f_after); + if (before_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED || + after_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = end.end_id; + return GEODESIC_ADVANCE_TERMINATED; + } + if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + if (f_before > 0.0 || f_after <= 0.0) + continue; + State crossing; + const AsymptoticStatus localized = localize_worldtube_crossing( + slab, end.end_id, &before, h, &crossing, &rhs_total); + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = end.end_id; + return GEODESIC_ADVANCE_TERMINATED; + } + if (localized != ASYMPTOTIC_OK) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + const AsymptoticStatus transfer = asymptotic_finish_escape( + slab->source, end.end_id, crossing.coordinate_time, crossing.x, + crossing.Pi, crossing.log_alpha_p0, out); + if (transfer == ASYMPTOTIC_OK) { + crossing.steps = s->steps; + crossing.log_alpha_p0_0 = s->log_alpha_p0_0; + crossing.integration_start_time = s->integration_start_time; + crossing.next_step = s->next_step; + crossing.rejected_steps = s->rejected_steps; + crossing.rhs_evaluations = s->rhs_evaluations; + crossing.previous_rejected = s->previous_rejected; + record_final_state(out, &crossing); + out->threshold_value = NAN; + return GEODESIC_ADVANCE_TERMINATED; + } + if (transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = end.end_id; + return GEODESIC_ADVANCE_TERMINATED; + } + restore_accepted_trajectory(s, &before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); return GEODESIC_ADVANCE_FAILED; } + continue; + } + + /* DP54: accept one step, enumerate the dense event candidates, and retry + * with a smaller step only when a candidate cannot be confirmed on the + * error-controlled trajectory (a polynomial-only artifact must never + * terminate the ray). */ + unsigned int event_consecutive = 0; + for (;;) { + const State step_before = *s; + AcceptedStep dense; + RayReason reason = RAY_REASON_INTEGRATION_ERROR; + if (dp_advance_one(slab, config, s, event_left, &reason, &rhs_total, + &reject_total, &dense)) { + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + set_incomplete(out, reason, s); + return GEODESIC_ADVANCE_FAILED; + } + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + const double h = s->coordinate_time - step_before.coordinate_time; + + /* The accepted-step event layer runs for every backend. With no declared + * ends the escape enumeration below is empty, but the L-threshold events + * still apply, so the legacy classifier path and the energy policy stay + * consistent for a no-end backend too. */ + + /* Enumerate escape candidates per end and localize each on the actual + * trajectory before choosing: the dense parameter ordering alone is not + * trusted. A strictly positive endpoint F with an inside (or boundary) + * start is a crossing even when the polynomial absorbed a near-endpoint + * root; F_after == 0 keeps the next-step boundary-root handling. */ + SpacetimeEndId escape_end = SPACETIME_END_NONE; + int escape_has = 0; + double escape_time = -INFINITY; + State escape_state = step_before; + int retry_step = 0; + int nonconstant = 0; + AsymptoticStatus end_fail = ASYMPTOTIC_OK; + SpacetimeEndId end_fail_id = SPACETIME_END_NONE; + for (size_t i = 0; i < end_count; ++i) { + SpacetimeAsymptoticEnd end; + if (spacetime_asymptotic_end(slab->source, i, &end)) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + double f_before, f_after; + const int before_status = asymptotic_worldtube_value( + slab->source, end.end_id, step_before.coordinate_time, + step_before.x, &f_before); + const int after_status = asymptotic_worldtube_value( + slab->source, end.end_id, s->coordinate_time, s->x, &f_after); + if (before_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED || + after_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + end_fail = ASYMPTOTIC_TIME_RANGE_EXHAUSTED; + end_fail_id = end.end_id; + break; + } + if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) { + end_fail = ASYMPTOTIC_INVALID; + break; + } + /* Local geometric ULP of F = |x-c|^2 - R^2, used to distinguish an + * entry-roundoff boundary state from a clearly-outside one. */ + double geom_tol = 0.0; + { + SpacetimeEscapeWorldtubeSample sample; + if (spacetime_escape_worldtube_sample(slab->source, end.end_id, + step_before.coordinate_time, + &sample) == 0 && + sample.valid && sample.radius > 0.0) { + const double r2 = sample.radius * sample.radius; + double d2 = 0.0; + for (int k = 0; k < 3; ++k) { + const double dk = step_before.x[k] - sample.center[k]; + d2 += dk * dk; + } + geom_tol = 128.0 * DBL_EPSILON * fmax(r2, d2); + } + } + if (f_before > geom_tol) { + /* Clearly outside this end. With a single end this is an + * inconsistent state the tracer must not search until the budget; + * a multi-end ray legitimately sits outside some ends. */ + if (end_count == 1) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + continue; + } + int has_bracket = 0; + double b_lo = 0.0, b_hi = 1.0; + if (f_before <= 0.0) { + double coeff[EVENT_POLY_MAX_DEGREE + 1]; + const int build = worldtube_dense_polynomial(slab->source, end.end_id, + &dense, coeff); + if (build < 0) { + nonconstant = 1; + break; + } + if (build == 1) { + const EventCrossing candidate = + poly_earliest_exit(coeff, EVENT_POLY_MAX_DEGREE); + if (candidate.has) { + has_bracket = 1; + b_lo = candidate.lo; + b_hi = candidate.hi; + } + } + } + if (!has_bracket) { + /* Whole-step fallback for a crossing the dense enumeration may have + * absorbed at theta == 1. A boundary start (0 < F_before <= + * geom_tol) counts as inside; a single touch (F_after == 0) is left + * for the following step's boundary root. */ + if (!(f_before <= geom_tol && f_after > 0.0)) + continue; + b_lo = 0.0; + b_hi = 1.0; + } + double f_lo, f_hi; + State lo_state, hi_state; + if (b_lo == 0.0 && b_hi == 1.0) { + /* The whole-step bracket endpoints are the accepted states already + * at hand; a boundary start (0 < F_before <= geom_tol) counts as + * inside for the bracket. */ + f_lo = (f_before < 0.0) ? f_before : 0.0; + f_hi = f_after; + } else { + RayReason verify_reason = RAY_REASON_PROTOCOL_ERROR; + if (dp_subintegrate(slab, config, &step_before, + step_before.coordinate_time + h * b_lo, &lo_state, + &verify_reason, &rhs_total, &reject_total) || + dp_subintegrate(slab, config, &step_before, + step_before.coordinate_time + h * b_hi, &hi_state, + &verify_reason, &rhs_total, &reject_total)) { + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, verify_reason, s); + return GEODESIC_ADVANCE_FAILED; + } + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + const int lo_status = asymptotic_worldtube_value( + slab->source, end.end_id, lo_state.coordinate_time, lo_state.x, + &f_lo); + const int hi_status = asymptotic_worldtube_value( + slab->source, end.end_id, hi_state.coordinate_time, hi_state.x, + &f_hi); + if (lo_status != ASYMPTOTIC_OK || hi_status != ASYMPTOTIC_OK) { + end_fail = ASYMPTOTIC_INVALID; + break; + } + } + if (!(f_lo <= 0.0 && f_hi > 0.0)) { + if (event_retry_limit(s, config, &step_before, &event_consecutive, + &reject_total)) { + s->rhs_evaluations = rhs_total; + set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + retry_step = 1; + break; + } + State crossing; + RayReason event_reason = RAY_REASON_PROTOCOL_ERROR; + const AsymptoticStatus localized = dp_localize_worldtube_crossing( + slab, config, end.end_id, &step_before, h, b_lo, b_hi, &crossing, + &event_reason, &rhs_total, &reject_total); + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = end.end_id; + return GEODESIC_ADVANCE_TERMINATED; + } + if (localized != ASYMPTOTIC_OK) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, event_reason, s); + return GEODESIC_ADVANCE_FAILED; + } + if (!escape_has || crossing.coordinate_time > escape_time) { + escape_has = 1; + escape_time = crossing.coordinate_time; + escape_state = crossing; + escape_end = end.end_id; + } + } + if (nonconstant) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_UNSUPPORTED, s); + return GEODESIC_ADVANCE_FAILED; + } + if (end_fail == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = end_fail_id; + return GEODESIC_ADVANCE_TERMINATED; + } + if (end_fail != ASYMPTOTIC_OK) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + if (retry_step) + continue; + + /* L-threshold events are localized for the L-based policies even when + * the accepted endpoint is already below the threshold again: the dense + * quartic supplies the earliest upcrossing bracket, and the actual + * error-controlled subintegration confirms that local bracket before any + * bisection. A non-monotonic L pulse must not be lost just because + * monitored(main endpoint) < threshold. LOG_P0 keeps its trusted-state + * semantics. */ + const int localizable_threshold = + config->threshold.kind == THRESHOLD_LOG_ENERGY_GROWTH || + config->threshold.kind == THRESHOLD_LOG_ALPHA_P0; + int threshold_has = 0; + double threshold_time = -INFINITY; + State threshold_state = step_before; + if (localizable_threshold) { + double tcoeff[5]; + const int have_coeff = threshold_dense_polynomial(config, &dense, tcoeff); + EventCrossing dense_thr = {0}; + int have_dense_thr = 0; + if (have_coeff) { + dense_thr = poly_earliest_exit(tcoeff, 4); + have_dense_thr = dense_thr.has; + } + /* Use the actual accepted endpoint, not the dense endpoint, for the + * closed (>=) event: the dense column-sum identity can miss equality + * by one rounding step. */ + const int endpoint_reaches = + monitored_threshold_value(slab, config->threshold.kind, s) >= + config->threshold.value; + + int have_bracket = 0; + double b_lo = 0.0, b_hi = 1.0; + if (have_dense_thr) { + have_bracket = 1; + b_lo = dense_thr.lo; + b_hi = dense_thr.hi; + } else if (endpoint_reaches) { + /* Closed-end candidate: the threshold (a >= event) is reached exactly + * at the step/slab endpoint, or a dense root was absorbed at + * theta == 1. poly_earliest_exit already proved there is no earlier + * upcrossing, so the last monotone segment ending at 1 is a valid + * local bracket. This keeps a legitimate endpoint threshold out of + * a permanent shrink. */ + have_bracket = 1; + b_lo = have_coeff ? poly_last_segment_start(tcoeff, 4) : 0.0; + b_hi = 1.0; + } + + if (have_bracket) { + RayReason tr_reason = RAY_REASON_INTEGRATION_ERROR; + const int ok = dp_localize_threshold_bracket( + slab, config, &step_before, h, b_lo, b_hi, &threshold_state, + &tr_reason, &rhs_total, &reject_total); + s->rhs_evaluations = rhs_total; + s->rejected_steps = reject_total; + if (ok < 0) { + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, tr_reason, s); + return GEODESIC_ADVANCE_FAILED; + } + if (ok == 0) { + /* The candidate cannot be confirmed on the actual trajectory. + * Shrink the accepted step and retry; when that is exhausted this + * is an unconfirmable event, reported as an integration failure + * with the last trusted state rather than rewritten into DARK. */ + if (event_retry_limit(s, config, &step_before, &event_consecutive, + &reject_total)) { + s->rhs_evaluations = rhs_total; + set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + continue; + } + threshold_has = 1; + threshold_time = threshold_state.coordinate_time; + } else { + /* The dense polynomial found no root at all. If the actual escape + * crossing already reaches the threshold, prefer shrinking so a + * consistent dense candidate can form; never bisect a whole + * multi-root prefix to claim a later crossing as the first. */ + const int actual_reaches = + escape_has && + monitored_threshold_value(slab, config->threshold.kind, + &escape_state) >= + config->threshold.value; + if (actual_reaches) { + if (event_retry_limit(s, config, &step_before, &event_consecutive, + &reject_total)) { + s->rhs_evaluations = rhs_total; + set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); + return GEODESIC_ADVANCE_FAILED; + } + continue; + } + } + } + if (!escape_has && !threshold_has) + break; + + /* Actual-time comparison at the local coordinate-time resolution; an + * indistinguishable pair keeps the documented escape priority. The ODE + * tolerance is not used as a time tie: a physically-resolved threshold + * gap stays a real difference. */ + const double tie_tol = + fmax(4.0 * time_ulp(step_before.coordinate_time), 1e-12 * fabs(h)); + const int dark = + threshold_has && + (!escape_has || threshold_time > escape_time + tie_tol); + + if (dark) { + State stop = threshold_state; + stop.steps = s->steps; + stop.log_alpha_p0_0 = s->log_alpha_p0_0; + stop.integration_start_time = s->integration_start_time; + stop.next_step = s->next_step; + stop.rejected_steps = s->rejected_steps; + stop.rhs_evaluations = s->rhs_evaluations; + stop.previous_rejected = s->previous_rejected; + out->outcome = RAY_OUTCOME_DARK; + out->reason = RAY_REASON_REDSHIFT_LIMIT; + out->end_id = SPACETIME_END_NONE; + record_final_state(out, &stop); + out->threshold_value = + monitored_threshold_value(slab, config->threshold.kind, &stop); + return GEODESIC_ADVANCE_TERMINATED; + } + if (!escape_has) + break; + State crossing = escape_state; const AsymptoticStatus transfer = asymptotic_finish_escape( - slab->source, end.end_id, crossing.coordinate_time, crossing.x, + slab->source, escape_end, crossing.coordinate_time, crossing.x, crossing.Pi, crossing.log_alpha_p0, out); if (transfer == ASYMPTOTIC_OK) { + crossing.steps = s->steps; + crossing.log_alpha_p0_0 = s->log_alpha_p0_0; + crossing.integration_start_time = s->integration_start_time; + crossing.next_step = s->next_step; + crossing.rejected_steps = s->rejected_steps; + crossing.rhs_evaluations = s->rhs_evaluations; + crossing.previous_rejected = s->previous_rejected; record_final_state(out, &crossing); out->threshold_value = NAN; return GEODESIC_ADVANCE_TERMINATED; } if (transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { - set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before); - out->end_id = end.end_id; + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, s); + out->end_id = escape_end; return GEODESIC_ADVANCE_TERMINATED; } - set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before); + restore_accepted_trajectory(s, &step_before); + set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); return GEODESIC_ADVANCE_FAILED; } } @@ -449,6 +1916,19 @@ GeodesicAdvanceResult geodesic_advance_past_ray( * budget-unresolved and require another slab/retry. */ if (threshold_reached(slab, config, s, out)) return GEODESIC_ADVANCE_TERMINATED; + if (use_dp && lookback_left >= slab_left_time && + s->coordinate_time <= + lookback_left + 2.0 * time_ulp(lookback_left)) { + /* The explicit DP54 lookback budget, not a slab data boundary, is what + * stopped a still-trustworthy trajectory: retryable UNRESOLVED. A source + * slab data time hole is instead reported where the metric eval fails. */ + out->outcome = RAY_OUTCOME_UNRESOLVED; + out->reason = RAY_REASON_BUDGET_EXHAUSTED; + out->end_id = SPACETIME_END_NONE; + record_final_state(out, s); + out->threshold_value = NAN; + return GEODESIC_ADVANCE_TERMINATED; + } return GEODESIC_ADVANCE_ACTIVE; } @@ -465,9 +1945,25 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source, .accepted_steps = 0, .threshold_value = NAN}; record_final_state(&out, NULL); - if (!source || !observer || !config || config->coordinate_time_step <= 0 || - !config->max_steps || fabs(dot(n, n) - 1) > 1e-10) + if (!source || !observer || !config || fabs(dot(n, n) - 1) > 1e-10) return out; + if (!stepper_known(config->stepper)) { + out.outcome = RAY_OUTCOME_INCOMPLETE; + out.reason = RAY_REASON_PROTOCOL_ERROR; + return out; + } + const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; + if (use_dp) { + if (!dp_config_valid(config)) { + out.outcome = RAY_OUTCOME_INCOMPLETE; + out.reason = RAY_REASON_PROTOCOL_ERROR; + return out; + } + } else if (config->coordinate_time_step <= 0 || !config->max_steps) { + return out; + } + out.lookback_limit = use_dp ? config->max_lookback_time : 0.0; + out.accepted_step_limit = config->max_steps; AsymptoticRoute route; const AsymptoticStatus route_status = asymptotic_route_camera(source, observer, n, &route); @@ -515,9 +2011,21 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source, /* Camera-event reference, distinct from the entry-state L for * an external camera. */ .log_alpha_p0_0 = route.log_alpha_p0_camera, - .steps = 0}; + .steps = 0, + /* Adaptive window starts at activation; the initial trial + * step reuses coordinate_time_step. */ + .integration_start_time = route.activate_t, + .next_step = config->coordinate_time_step, + .rejected_steps = 0, + .rhs_evaluations = 0, + .previous_rejected = 0}; + /* Legacy RK4 still sizes its history from coordinate_time_step * max_steps; + * DP54 instead uses the explicit max_lookback_time and never derives its + * history from that product. */ const double last_time = - route.activate_t - config->coordinate_time_step * config->max_steps; + use_dp ? route.activate_t - config->max_lookback_time + : route.activate_t - + config->coordinate_time_step * config->max_steps; MetricSlab *slab = NULL; if (spacetime_load_slab(source, route.activate_t, last_time - 1.0, &slab)) { out.outcome = RAY_OUTCOME_INCOMPLETE; @@ -546,13 +2054,38 @@ RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source, .accepted_steps = 0, .threshold_value = NAN}; record_final_state(&out, NULL); - if (!source || !state_in || !config || config->coordinate_time_step <= 0 || - !config->max_steps || state_in->steps >= config->max_steps) + if (!source || !state_in || !config || !config->max_steps || + state_in->steps >= config->max_steps) return out; + if (!stepper_known(config->stepper)) { + out.outcome = RAY_OUTCOME_INCOMPLETE; + out.reason = RAY_REASON_PROTOCOL_ERROR; + return out; + } + const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; + if (use_dp) { + if (!dp_config_valid(config)) { + out.outcome = RAY_OUTCOME_INCOMPLETE; + out.reason = RAY_REASON_PROTOCOL_ERROR; + return out; + } + } else if (config->coordinate_time_step <= 0) { + return out; + } + out.lookback_limit = use_dp ? config->max_lookback_time : 0.0; + out.accepted_step_limit = config->max_steps; State state = *state_in; - const double remaining = - config->coordinate_time_step * (double)(config->max_steps - state.steps); - const double last_time = state.coordinate_time - remaining - 1.0; + if (use_dp && !isfinite(state.integration_start_time)) + state.integration_start_time = state.coordinate_time; + /* Resuming must not reset L0 or the adaptive window/cost: they are carried + * in `state`. An explicit larger max_lookback_time extends the retry. */ + const double last_time = + use_dp + ? state.integration_start_time - config->max_lookback_time + : state.coordinate_time - + config->coordinate_time_step * + (double)(config->max_steps - state.steps) - + 1.0; MetricSlab *slab = NULL; if (spacetime_load_slab(source, state.coordinate_time, last_time, &slab)) { out.outcome = RAY_OUTCOME_INCOMPLETE; diff --git a/src/geodesic.h b/src/geodesic.h index 20b97fb..786ffd0 100644 --- a/src/geodesic.h +++ b/src/geodesic.h @@ -56,7 +56,13 @@ typedef struct { /* Last trusted state at termination. For an ESCAPED endpoint this is the * (finite) numerical truncation position, not the true parameter end. */ double stop_coordinate_time; - unsigned int accepted_steps; + unsigned int accepted_steps; /* accepted (spent) steps */ + /* Actual total accepted-step budget the trace was configured with, kept + * distinct from the spent `accepted_steps`. Production endpoints always + * carry it so a retry layer never has to infer a grant from the spent count; + * zero only on legacy/synthetic endpoints. For a resumed trace this is the + * new total grant, not the additional steps. */ + unsigned int accepted_step_limit; /* Last trusted continuous state, used to resume an UNRESOLVED ray from its * last accepted step instead of replaying it from the camera. */ double final_x[3]; @@ -65,14 +71,73 @@ typedef struct { double final_log_alpha_p0_0; /* original reference L0 for retries */ /* Monitored threshold value at termination, NAN when not applicable. */ double threshold_value; + /* Adaptive (DP54) controller state and cumulative cost at termination, + * copied from the last trusted ray state so a retry resumes without + * replaying. The controller fields below (integration window, suggested + * step, previous-rejection flag, rejected-step count) are not applicable to + * RK4 and stay zero there. `rhs_evaluations` is different: it is the real + * cumulative metric-evaluation cost for every stepper, RK4 included. */ + double integration_start_time; /* adaptive window start (entry/activation) */ + double next_step; /* positive trial-step magnitude, 0 if unset */ + unsigned int rejected_steps; /* cumulative rejected trials (DP54 only) */ + unsigned long rhs_evaluations; /* cumulative actual RHS evaluations */ + unsigned int previous_rejected;/* 1 when the last trial was rejected */ + /* Coordinate-time lookback budget actually granted to this trace, recorded + * so the frame/movie retry layer can accumulate the per-sample total + * independently of the accepted-step budget. Zero for the legacy RK4 path + * and for endpoints that never reached a trusted adaptive state. This is a + * resource quota, not part of the continuous resume state. */ + double lookback_limit; } RayEndpoint; +/* ODE stepper selection for the past-directed geodesic integrator. RK4 is + * the legacy fixed-step path and is value 0 so that zero-initialized configs + * keep their historical behavior. DP54 is the explicit adaptive + * Dormand-Prince 5(4) stepper; both paths share the same termination, + * lookback-budget and retry-control interfaces. */ +typedef enum { + GEODESIC_STEPPER_RK4 = 0, + GEODESIC_STEPPER_DP54 = 1 +} GeodesicStepper; + typedef struct { double coordinate_time_step; unsigned int max_steps; /* Normal dark terminal for every backend. No backend may substitute a * position/horizon cutoff for physical capture. */ ThresholdPolicy threshold; + + /* Stepper selection (see GeodesicStepper). */ + GeodesicStepper stepper; + + /* DP54 tolerances. When stepper == GEODESIC_STEPPER_DP54 all four must be + * finite and strictly positive; they are never silently defaulted. Error + * scales: position uses atol_x + rtol*max(|dx|, |h*dxdt|) so a distant + * coordinate origin cannot relax the local length scale; Pi uses + * atol_Pi + rtol*max(|Pi_before|, |Pi_candidate|); L uses + * atol_L + rtol*max(1, |dL|). */ + double atol_x; + double atol_Pi; + double atol_L; + double rtol; + + /* DP54 trial-step magnitude bounds in coordinate time. Both finite and + * 0 < min_step <= max_step. The initial trial step and the driver + * suggestion reuse coordinate_time_step. */ + double min_step; + double max_step; + + /* DP54 maximum number of consecutive rejected trials before reporting an + * integration error. Must be nonzero. */ + unsigned int consecutive_rejection_limit; + + /* DP54 explicit coordinate-time lookback budget (length, finite and + * strictly positive). DP54 never derives its history from the legacy + * coordinate_time_step * max_steps product: reaching + * integration_start_time - max_lookback_time while the trajectory is still + * trustworthy is UNRESOLVED/BUDGET_EXHAUSTED, distinct from a source slab + * data time failure (TIME_RANGE_EXHAUSTED). */ + double max_lookback_time; } GeodesicTraceConfig; typedef struct { @@ -83,7 +148,19 @@ typedef struct { /* Reference L at the start of this ray's integration, carried unchanged * through retries so THRESHOLD_LOG_ENERGY_GROWTH stays camera-relative. */ double log_alpha_p0_0; - unsigned int steps; + unsigned int steps; /* accepted steps (all steppers) */ + /* Adaptive DP54 controller state; the controller fields default to zero and + * are ignored by the RK4 path. `integration_start_time` is the inner + * activation/entry time (not the L0 camera energy); `next_step` is a + * positive trial-step magnitude and is initialized from + * config->coordinate_time_step on the first adaptive advance. + * `rhs_evaluations` is the real cumulative RHS cost for every stepper; + * `rejected_steps`/`previous_rejected` remain DP54-only. */ + double integration_start_time; + double next_step; + unsigned int rejected_steps; /* cumulative rejected trials (DP54 only) */ + unsigned long rhs_evaluations; /* cumulative actual RHS evaluations */ + unsigned int previous_rejected;/* 1 when the last trial was rejected */ } GeodesicRayState; typedef enum { diff --git a/src/lens_map.c b/src/lens_map.c index f407c50..72a326a 100644 --- a/src/lens_map.c +++ b/src/lens_map.c @@ -11,9 +11,15 @@ * because their padding and size_t width are ABI-dependent. */ static const unsigned char lens_map_magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0}; /* Version 2 stores the two-level RayOutcome instead of the removed - * RayEndpointStatus. Version 1 files are rejected: their old captured bit - * cannot be upgraded into the new dark/unresolved/error provenance. */ -enum { LENS_MAP_VERSION = 2, LENS_MAP_ENDIAN = 0x01020304u }; + * RayEndpointStatus. Version 3 appends the adaptive DP54 policy and the + * per-vertex integration cost; it still supports v2 import as a legacy + * fixed-step RK4 map. Version 1 is rejected: its old captured bit cannot be + * upgraded into the new dark/unresolved/error provenance. */ +enum { + LENS_MAP_VERSION = 3, + LENS_MAP_VERSION_LEGACY = 2, + LENS_MAP_ENDIAN = 0x01020304u +}; static uint32_t crc32_update(uint32_t crc, const void *data, size_t size) { const unsigned char *bytes = data; @@ -63,6 +69,55 @@ static int unit_vector(const double v[3]) { const double n2 = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; return isfinite(n2) && fabs(n2 - 1.0) <= 1e-9; } + +/* Explicit switch over the stable wire codes; unknown codes are rejected + * instead of being reinterpreted as a C enum value. */ +static int integrator_code_known(uint32_t code) { + switch ((GeodesicStepper)code) { + case GEODESIC_STEPPER_RK4: + case GEODESIC_STEPPER_DP54: + return 1; + default: + return 0; + } +} + +/* Shared read/write schema. It deliberately does not validate mesh contents + * (valid_mesh does that per frame): it only proves the provenance is a + * complete, self-consistent policy. RK4 maps must state that no adaptive + * configuration applies; DP54 maps must carry it in full. */ +static int provenance_valid(const LensMapProvenance *p) { + if (p == NULL || !isfinite(p->threshold_value) || + p->threshold_kind > THRESHOLD_LOG_ENERGY_GROWTH || + !isfinite(p->min_edge_pixels) || p->min_edge_pixels < 0.0 || + !isfinite(p->min_area_pixels2) || p->min_area_pixels2 < 0.0 || + !isfinite(p->coordinate_time_step) || p->coordinate_time_step <= 0.0 || + !integrator_code_known(p->integrator)) + return 0; + if (p->integrator == (uint32_t)GEODESIC_STEPPER_RK4) { + return p->atol_x == 0.0 && p->atol_Pi == 0.0 && p->atol_L == 0.0 && + p->rtol == 0.0 && p->min_step == 0.0 && p->max_step == 0.0 && + p->max_lookback_time == 0.0 && p->retry_lookback_increment == 0.0 && + p->max_total_lookback_time == 0.0 && + p->max_consecutive_rejections == 0; + } + /* DP54. */ + return p->initial_max_steps != 0 && isfinite(p->atol_x) && p->atol_x > 0.0 && + isfinite(p->atol_Pi) && p->atol_Pi > 0.0 && + isfinite(p->atol_L) && p->atol_L > 0.0 && isfinite(p->rtol) && + p->rtol > 0.0 && isfinite(p->min_step) && p->min_step > 0.0 && + isfinite(p->max_step) && p->max_step > 0.0 && + p->min_step <= p->max_step && + p->coordinate_time_step >= p->min_step && + p->coordinate_time_step <= p->max_step && + isfinite(p->max_lookback_time) && p->max_lookback_time > 0.0 && + p->max_consecutive_rejections != 0 && + isfinite(p->retry_lookback_increment) && + p->retry_lookback_increment >= 0.0 && + isfinite(p->max_total_lookback_time) && + p->max_total_lookback_time >= p->max_lookback_time; +} + static int valid_mesh(const FrameLensMesh *m) { if (m == NULL || m->vertex_count == 0 || m->triangle_count == 0) return 0; for (size_t i = 0; i < m->vertex_count; ++i) { @@ -87,7 +142,8 @@ int lens_map_write(const char *path, int width, int height, double fov, const LensMapFrame *frames, size_t frame_count) { if (path == NULL || provenance == NULL || frames == NULL || width <= 0 || height <= 0 || !isfinite(fov) || fov <= 0.0 || fov >= 179.0 || - frame_count == 0 || frame_count > UINT64_MAX) + frame_count == 0 || frame_count > UINT64_MAX || + !provenance_valid(provenance)) return -1; for (size_t i = 0; i < frame_count; ++i) if (!valid_mesh(&frames[i].mesh)) return -1; FILE *file = fopen(path, "wb"); if (file == NULL) return -1; @@ -105,7 +161,17 @@ int lens_map_write(const char *path, int width, int height, double fov, write_double(file, provenance->min_edge_pixels, NULL) || write_double(file, provenance->min_area_pixels2, NULL) || write_double(file, provenance->coordinate_time_step, NULL) || - write_u32(file, provenance->initial_max_steps, NULL); + write_u32(file, provenance->initial_max_steps, NULL) || + write_double(file, provenance->atol_x, NULL) || + write_double(file, provenance->atol_Pi, NULL) || + write_double(file, provenance->atol_L, NULL) || + write_double(file, provenance->rtol, NULL) || + write_double(file, provenance->min_step, NULL) || + write_double(file, provenance->max_step, NULL) || + write_double(file, provenance->max_lookback_time, NULL) || + write_double(file, provenance->retry_lookback_increment, NULL) || + write_double(file, provenance->max_total_lookback_time, NULL) || + write_u32(file, provenance->max_consecutive_rejections, NULL); for (size_t f = 0; !failed && f < frame_count; ++f) { const FrameLensMesh *m = &frames[f].mesh; uint32_t crc = UINT32_MAX; failed = write_u64(file, frames[f].frame_id, NULL) || @@ -122,7 +188,10 @@ int lens_map_write(const char *path, int width, int height, double fov, failed = failed || write_double(file, v->log_frequency_ratio, &crc) || write_u32(file, (uint32_t)v->end_id, &crc) || write_u32(file, (uint32_t)v->outcome, &crc) || - write_u32(file, (uint32_t)v->reason, &crc); + write_u32(file, (uint32_t)v->reason, &crc) || + write_u64(file, v->trace_accepted_steps, &crc) || + write_u64(file, v->trace_rejected_steps, &crc) || + write_u64(file, v->trace_rhs_evaluations, &crc); } for (size_t i = 0; !failed && i < m->triangle_count; ++i) { for (int j = 0; j < 3; ++j) failed = failed || write_u64(file, m->triangles[i].vertex[j], &crc); @@ -147,11 +216,20 @@ int lens_map_read(const char *path, LensMapProvenance *provenance, *map = (LensMap){0}; FILE *file = fopen(path, "rb"); if (file == NULL) return -1; unsigned char magic[8]; uint32_t version, endian, width, height; uint64_t count; LensMapProvenance prov = {0}; - int failed = read_bytes(file, magic, sizeof magic, NULL) || memcmp(magic, lens_map_magic, sizeof magic) || + int failed = read_bytes(file, magic, sizeof magic, NULL) || + memcmp(magic, lens_map_magic, sizeof magic) || read_u32(file, &version, NULL) || read_u32(file, &endian, NULL) || read_u32(file, &width, NULL) || read_u32(file, &height, NULL) || - read_double(file, &map->horizontal_fov_deg, NULL) || read_u64(file, &count, NULL) || - read_u32(file, &prov.threshold_kind, NULL) || + read_double(file, &map->horizontal_fov_deg, NULL) || read_u64(file, &count, NULL); + /* Branch on the version before reading any version-specific header field, so + * a v2 file is never parsed with the v3 layout. */ + if (failed) + goto done; + if (version != LENS_MAP_VERSION && version != LENS_MAP_VERSION_LEGACY) { + failed = 1; + goto done; + } + failed = read_u32(file, &prov.threshold_kind, NULL) || read_u32(file, &prov.threshold_policy_version, NULL) || read_double(file, &prov.threshold_value, NULL) || read_u32(file, &prov.retry_step_increment, NULL) || @@ -161,16 +239,35 @@ int lens_map_read(const char *path, LensMapProvenance *provenance, read_double(file, &prov.min_edge_pixels, NULL) || read_double(file, &prov.min_area_pixels2, NULL) || read_double(file, &prov.coordinate_time_step, NULL) || - read_u32(file, &prov.initial_max_steps, NULL) || - version != LENS_MAP_VERSION || endian != LENS_MAP_ENDIAN || width == 0 || height == 0 || - width > INT32_MAX || height > INT32_MAX || !isfinite(map->horizontal_fov_deg) || - map->horizontal_fov_deg <= 0.0 || map->horizontal_fov_deg >= 179.0 || count == 0 || - prov.threshold_kind > THRESHOLD_LOG_ENERGY_GROWTH || - !isfinite(prov.threshold_value) || - prov.coordinate_time_step < 0.0 || !isfinite(prov.coordinate_time_step) || - count > SIZE_MAX / sizeof *map->frames; + read_u32(file, &prov.initial_max_steps, NULL); + if (!failed && version == LENS_MAP_VERSION) { + failed = read_double(file, &prov.atol_x, NULL) || + read_double(file, &prov.atol_Pi, NULL) || + read_double(file, &prov.atol_L, NULL) || + read_double(file, &prov.rtol, NULL) || + read_double(file, &prov.min_step, NULL) || + read_double(file, &prov.max_step, NULL) || + read_double(file, &prov.max_lookback_time, NULL) || + read_double(file, &prov.retry_lookback_increment, NULL) || + read_double(file, &prov.max_total_lookback_time, NULL) || + read_u32(file, &prov.max_consecutive_rejections, NULL); + } + /* Legacy v2 carried no adaptive policy. Its missing fields stay zero, and + * its integrator must be RK4: a v2 DP54 claim is impossible and rejected. */ + if (!failed && version == LENS_MAP_VERSION_LEGACY && + prov.integrator != (uint32_t)GEODESIC_STEPPER_RK4) + failed = 1; + if (!failed && + (endian != LENS_MAP_ENDIAN || width == 0 || height == 0 || + width > INT32_MAX || height > INT32_MAX || + !isfinite(map->horizontal_fov_deg) || + map->horizontal_fov_deg <= 0.0 || map->horizontal_fov_deg >= 179.0 || + count == 0 || count > SIZE_MAX / sizeof *map->frames || + !provenance_valid(&prov))) + failed = 1; if (failed) goto done; map->provenance = prov; + map->file_version = version; if (provenance != NULL) *provenance = prov; map->width = (int)width; map->height = (int)height; map->frame_count = (size_t)count; @@ -204,6 +301,11 @@ int lens_map_read(const char *path, LensMapProvenance *provenance, v->outcome = (RayOutcome)outcome; v->reason = (RayReason)reason; v->traced = 1; + if (version == LENS_MAP_VERSION) { + failed = failed || read_u64(file, &v->trace_accepted_steps, &crc) || + read_u64(file, &v->trace_rejected_steps, &crc) || + read_u64(file, &v->trace_rhs_evaluations, &crc); + } } for (size_t i = 0; !failed && i < frame->mesh.triangle_count; ++i) { for (int j = 0; j < 3; ++j) { diff --git a/src/lens_map.h b/src/lens_map.h index 5f5dbd6..26da53e 100644 --- a/src/lens_map.h +++ b/src/lens_map.h @@ -25,19 +25,46 @@ typedef struct { uint32_t retry_step_increment; uint32_t max_total_steps; uint32_t max_level; - uint32_t integrator; /* 0 = fixed-step RK4 (transitional) */ + /* Stable wire code: 0 = fixed-step RK4, 1 = adaptive Dormand-Prince 5(4). + * Never serialize the C enum directly; unknown codes are rejected. */ + uint32_t integrator; double min_edge_pixels; double min_area_pixels2; /* Integration source: the coordinate-time step and the initial per-ray * accepted-step budget used for the first trace. */ double coordinate_time_step; uint32_t initial_max_steps; + /* Adaptive DP54 configuration. For an RK4 map every field here is exactly + * zero, which makes "not applicable" explicit on the wire instead of + * implying a silently defaulted stepper. For a DP54 map the tolerances, + * step bounds and explicit lookback budget are finite and strictly positive, + * min_step <= coordinate_time_step <= max_step, and the consecutive + * rejection limit is nonzero. */ + double atol_x; + double atol_Pi; + double atol_L; + double rtol; + double min_step; + double max_step; + double max_lookback_time; + /* Retry resource policy: coordinate-time increment per retry and the total + * lookback cap. `retry_lookback_increment` may be zero (fixed time budget); + * a production DP54 map requires max_total_lookback_time >= + * max_lookback_time (the initial grant). Both zero on an RK4 map. */ + double retry_lookback_increment; + double max_total_lookback_time; + uint32_t max_consecutive_rejections; } LensMapProvenance; typedef struct { int width, height; double horizontal_fov_deg; LensMapProvenance provenance; + /* On-disk format version actually read (2 for legacy RK4, 3 for the current + * adaptive-aware format). 0 when the struct was not produced by a read. + * A v2 map carries no per-vertex integration cost, so those replay as zero; + * the unknown cost is explicit through this version rather than implied. */ + uint32_t file_version; LensMapFrame *frames; size_t frame_count; } LensMap; diff --git a/src/main.c b/src/main.c index 956fc88..9fa029f 100644 --- a/src/main.c +++ b/src/main.c @@ -10,6 +10,8 @@ #include "spacetime.h" #include +#include +#include #include #include #include @@ -67,6 +69,24 @@ typedef struct { const char *fast_fftw_wisdom_path; const char *blackbody_table_path; RefinementConfig refinement; + /* Integrator selection and its explicit-override flags. A flag records that + * a value was named on the command line so an explicit zero is never confused + * with "not specified, use the derived default". */ + GeodesicStepper stepper; + int integrator_specified; + double ode_rtol, ode_atol_x, ode_atol_pi, ode_atol_l; + double ode_initial_step, ode_min_step, ode_max_step; + unsigned int ode_max_rejections; + unsigned int trace_max_steps; + double trace_lookback_time; + double retry_lookback_increment; + double max_total_lookback_time; + int ode_rtol_specified, ode_atol_x_specified, ode_atol_pi_specified; + int ode_atol_l_specified, ode_initial_step_specified, ode_min_step_specified; + int ode_max_step_specified, ode_max_rejections_specified; + int trace_max_steps_specified, trace_lookback_time_specified; + int retry_step_increment_specified, max_total_steps_specified; + int retry_lookback_increment_specified, max_total_lookback_time_specified; ToneMapSettings tone_map; PngWriteSettings png; int sensor_bloom_enabled; @@ -76,6 +96,15 @@ typedef struct { double sensor_bloom_transfer; } Settings; +/* Keep the production default independent of backend-specific units and + * resource allowances, which are populated by trace_config(). */ +static GeodesicStepper default_stepper(void) { return GEODESIC_STEPPER_DP54; } + +static GeodesicTraceConfig trace_config(const Settings *s); +static RefinementConfig effective_refinement(const Settings *s, + const GeodesicTraceConfig *trace); +static int validate_settings(const Settings *s); + static int parse_int(const char *text, int *value) { char *end; errno = 0; @@ -162,6 +191,27 @@ static int parse_finite_positive(const char *text, double *value) { return errno || end == text || *end || !isfinite(*value) || *value <= 0.0 ? -1 : 0; } +/* Unsigned integer with the full 32-bit range, zero allowed. Used by retry + * quotas where an explicit zero is meaningful. */ +static int parse_uint_nonnegative(const char *text, unsigned int *value) { + char *end; + errno = 0; + const unsigned long parsed = strtoul(text, &end, 10); + if (errno || end == text || *end || parsed > UINT_MAX) + return -1; + *value = (unsigned int)parsed; + return 0; +} + +/* Unsigned integer that must be at least one. */ +static int parse_uint_positive(const char *text, unsigned int *value) { + unsigned int parsed; + if (parse_uint_nonnegative(text, &parsed) || parsed == 0) + return -1; + *value = parsed; + return 0; +} + /* Sensor-bloom transfer coefficient: finite and in [0, 1). */ static int parse_sensor_bloom_transfer(const char *text, double *value) { char *end; @@ -406,6 +456,7 @@ static int parse_args(int argc, char **argv, Settings *s, .catalog_load_workers = 4, .movie_output_workers = 0, .fast_fftw_plan_mode = FAST_PSF_FFTW_PLAN_ESTIMATE, + .stepper = default_stepper(), .refinement = {.angle_absolute_rad = 1e-3 * 3.14159265358979323846 / 180.0, .angle_relative = 0.1, @@ -474,6 +525,60 @@ static int parse_args(int argc, char **argv, Settings *s, !parse_positive(argv[++i], &s->exposure)) { } else if (!strcmp(argv[i], "--dark-threshold") && i + 1 < argc && !parse_positive(argv[++i], &s->dark_threshold)) { + } else if (!strcmp(argv[i], "--integrator") && i + 1 < argc) { + const char *mode = argv[++i]; + if (!strcmp(mode, "rk4")) { + s->stepper = GEODESIC_STEPPER_RK4; + } else if (!strcmp(mode, "dp54")) { + s->stepper = GEODESIC_STEPPER_DP54; + } else { + fprintf(stderr, "--integrator expects rk4 or dp54.\n"); + return -1; + } + s->integrator_specified = 1; + } else if (!strcmp(argv[i], "--ode-rtol") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_rtol)) { + s->ode_rtol_specified = 1; + } else if (!strcmp(argv[i], "--ode-atol-x") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_atol_x)) { + s->ode_atol_x_specified = 1; + } else if (!strcmp(argv[i], "--ode-atol-pi") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_atol_pi)) { + s->ode_atol_pi_specified = 1; + } else if (!strcmp(argv[i], "--ode-atol-l") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_atol_l)) { + s->ode_atol_l_specified = 1; + } else if (!strcmp(argv[i], "--ode-initial-step") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_initial_step)) { + s->ode_initial_step_specified = 1; + } else if (!strcmp(argv[i], "--ode-min-step") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_min_step)) { + s->ode_min_step_specified = 1; + } else if (!strcmp(argv[i], "--ode-max-step") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->ode_max_step)) { + s->ode_max_step_specified = 1; + } else if (!strcmp(argv[i], "--ode-max-rejections") && i + 1 < argc && + !parse_uint_positive(argv[++i], &s->ode_max_rejections)) { + s->ode_max_rejections_specified = 1; + } else if (!strcmp(argv[i], "--trace-max-steps") && i + 1 < argc && + !parse_uint_positive(argv[++i], &s->trace_max_steps)) { + s->trace_max_steps_specified = 1; + } else if (!strcmp(argv[i], "--trace-lookback-time") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->trace_lookback_time)) { + s->trace_lookback_time_specified = 1; + } else if (!strcmp(argv[i], "--retry-step-increment") && i + 1 < argc && + !parse_uint_nonnegative(argv[++i], + &s->refinement.retry_step_increment)) { + s->retry_step_increment_specified = 1; + } else if (!strcmp(argv[i], "--max-total-steps") && i + 1 < argc && + !parse_uint_positive(argv[++i], &s->refinement.max_total_steps)) { + s->max_total_steps_specified = 1; + } else if (!strcmp(argv[i], "--retry-lookback-increment") && i + 1 < argc && + !parse_nonnegative(argv[++i], &s->retry_lookback_increment)) { + s->retry_lookback_increment_specified = 1; + } else if (!strcmp(argv[i], "--max-total-lookback-time") && i + 1 < argc && + !parse_finite_positive(argv[++i], &s->max_total_lookback_time)) { + s->max_total_lookback_time_specified = 1; } else if (!strcmp(argv[i], "--tone-map") && i + 1 < argc) { const char *mode = argv[++i]; if (!strcmp(mode, "softclip")) @@ -615,7 +720,7 @@ static int parse_args(int argc, char **argv, Settings *s, return -1; } s->sensor_bloom_enabled = s->sensor_bloom_limit_specified; - return 0; + return validate_settings(s); } static void print_help(const char *program) { @@ -719,6 +824,26 @@ static void print_help(const char *program) { #endif fputs(" --allow-incomplete Publish even when unresolved/error rays remain (diagnostic; output is marked incomplete)\n", stdout); + fputs( + "\nIntegrator (adaptive DP5(4); fixed RK4 for comparison):\n" + " --integrator rk4|dp54 Past-ray stepper (default: dp54)\n" + " --ode-rtol R DP54 relative tolerance (default: 1e-9)\n" + " --ode-atol-x A DP54 position absolute tolerance (default: 1e-9 * backend length)\n" + " --ode-atol-pi A DP54 Pi absolute tolerance (default: 1e-9)\n" + " --ode-atol-l A DP54 log-energy absolute tolerance (default: 1e-9)\n" + " --ode-initial-step H Initial trial step in coordinate time (default: backend step)\n" + " --ode-min-step H DP54 minimum step (default: max(1e-12, 16 eps) * backend unit)\n" + " --ode-max-step H DP54 maximum step (default: backend dependent)\n" + " --ode-max-rejections N Consecutive rejected trials before failure (default: 32)\n" + " --trace-max-steps N Initial accepted-step budget (default: backend budget)\n" + " --trace-lookback-time T Explicit DP54 coordinate-time lookback budget\n" + " --retry-step-increment N Accepted steps added per retry (0 disables step retries)\n" + " --max-total-steps N Hard per-ray accepted-step cap across retries\n" + " --retry-lookback-increment T Coordinate-time budget added per retry (0 keeps it fixed)\n" + " --max-total-lookback-time T Hard per-ray coordinate-time cap across retries\n" + " DP54-only options are rejected under --integrator rk4;\n" + " all tracing options are rejected with --lens-map-input.\n", + stdout); #ifdef SPACETIME_ALCUBIERRE fputs( "\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no capture):\n" @@ -851,9 +976,10 @@ static void report_frame_refinement(void *context, size_t generation, } #ifdef SPACETIME_ALCUBIERRE -/* Upper bound on the per-ray step budget. Legal parameters whose worst-case - * near-comoving ray could need more than this are rejected at startup rather - * than silently terminating as UNRESOLVED/BUDGET_EXHAUSTED. */ +/* Resource cap on the accepted-step estimate for the legacy fixed-step + * Alcubierre domain guard. It bounds a resource allowance only; the adaptive + * coordinate-time budget, not an accepted-step count, defines the trusted + * traced history and is never derived from this cap. */ #define ALCUBIERRE_MAX_TRACE_STEPS (1u << 24) /* Safety margin over the straight-line worst case: wall-region deflection can @@ -865,25 +991,33 @@ static double alcubierre_time_step(const Settings *s) { return fmin(0.1, 0.05 / s->alcubierre_sigma); } -/* Worst-case per-ray step budget, including ALCUBIERRE_BUDGET_MARGIN. 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 - * can take ~4*escape/(1-|v_s|) in coordinate time. trace_config() and the - * startup rejection share this single value so the configured limit always - * carries the full margin when it is accepted. */ -static double alcubierre_step_budget(const Settings *s) { +/* 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. */ +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 * alcubierre_time_step(s)); + return ALCUBIERRE_BUDGET_MARGIN * 4.0 * escape / separation; +} + +/* Legacy accepted-step estimate for the same coverage, used only by the + * fixed-step RK4 comparison path and to seed the initial accepted-step resource + * allowance. The adaptive path uses alcubierre_trace_time_budget() directly + * and never derives its history from a step count. */ +static double alcubierre_step_budget(const Settings *s) { + return alcubierre_trace_time_budget(s) / alcubierre_time_step(s); } #endif -/* Recompute approximate-black provenance and report E/D/U accounting. The - * result is diagnostic for now; production failure gating is layered on top - * of the same counters. */ +/* Recompute per-triangle approximate-black provenance and aggregate the E/D/U + * accounting across the given frames. The caller uses the returned totals both + * for the verbose report and for boundary_allows_publish() production gating. */ static void report_boundary_stats(const Settings *s, FrameLensMesh *const *meshes, size_t frame_count, @@ -927,10 +1061,50 @@ static void report_boundary_stats(const Settings *s, total.approx_black_level_stops); } +static uint64_t add_cost_u64(uint64_t total, uint64_t value, int *saturated) { + if (UINT64_MAX - total < value) { + *saturated = 1; + return UINT64_MAX; + } + return total + value; +} + +/* Sum a frame's persistent-vertex cost into a running total. Every vertex + * (including promoted probe witnesses) is counted once, so this is safe to + * call after refinement and across movie frames without double-counting a + * retried sample. */ +static void accumulate_trace_stats(const FrameLensMesh *mesh, + FrameTraceStats *total) { + FrameTraceStats frame; + frame_lens_mesh_trace_stats(mesh, &frame); + int saturated = 0; + total->accepted_steps = + add_cost_u64(total->accepted_steps, frame.accepted_steps, &saturated); + total->rejected_steps = + add_cost_u64(total->rejected_steps, frame.rejected_steps, &saturated); + total->rhs_evaluations = + add_cost_u64(total->rhs_evaluations, frame.rhs_evaluations, &saturated); + total->vertices += frame.vertices; + total->saturated_vertices += frame.saturated_vertices + (saturated ? 1u : 0u); +} + +static void report_trace_stats(const Settings *s, const FrameTraceStats *stats, + const char *label) { + if (!s->verbose) + return; + fprintf(stderr, + "%s trace cost: accepted=%" PRIu64 " rejected=%" PRIu64 + " rhs=%" PRIu64 " over %zu vertices (saturated=%zu).\n", + label, stats->accepted_steps, stats->rejected_steps, + stats->rhs_evaluations, stats->vertices, stats->saturated_vertices); +} + static LensMapProvenance lens_map_provenance(const Settings *s, const GeodesicTraceConfig *trace) { - RefinementConfig rc = s->refinement; - frame_retry_config_defaults(&rc, trace); + const RefinementConfig rc = effective_refinement(s, trace); + /* An RK4 map advertises "no adaptive configuration" as explicit zeros; the + * DP54 adaptive policy is only stored when the DP54 stepper actually ran. */ + const int dp = trace->stepper == GEODESIC_STEPPER_DP54; return (LensMapProvenance){.threshold_kind = (uint32_t)trace->threshold.kind, .threshold_policy_version = trace->threshold.policy_version, @@ -938,11 +1112,24 @@ static LensMapProvenance lens_map_provenance(const Settings *s, .retry_step_increment = rc.retry_step_increment, .max_total_steps = rc.max_total_steps, .max_level = rc.max_level, - .integrator = 0, + .integrator = (uint32_t)trace->stepper, .min_edge_pixels = rc.min_edge_pixels, .min_area_pixels2 = rc.min_area_pixels2, .coordinate_time_step = trace->coordinate_time_step, - .initial_max_steps = trace->max_steps}; + .initial_max_steps = trace->max_steps, + .atol_x = dp ? trace->atol_x : 0.0, + .atol_Pi = dp ? trace->atol_Pi : 0.0, + .atol_L = dp ? trace->atol_L : 0.0, + .rtol = dp ? trace->rtol : 0.0, + .min_step = dp ? trace->min_step : 0.0, + .max_step = dp ? trace->max_step : 0.0, + .max_lookback_time = dp ? trace->max_lookback_time : 0.0, + .retry_lookback_increment = + dp ? rc.retry_lookback_increment : 0.0, + .max_total_lookback_time = + dp ? rc.max_total_lookback_time : 0.0, + .max_consecutive_rejections = + dp ? trace->consecutive_rejection_limit : 0u}; } /* Refuse to publish silently on true errors or on unresolved triangles that @@ -976,30 +1163,207 @@ static GeodesicTraceConfig trace_config(const Settings *s) { const ThresholdPolicy threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH, .value = s->dark_threshold, .policy_version = 3}; -#ifdef SPACETIME_SCHWARZSCHILD - /* The directed worldtube crossing makes an escaping ray traverse the - * interior as a round trip from the entry sphere (in, turn, back out), - * rather than the old one-way stop at the first outside sample. The step - * budget must cover roughly twice the escape sphere plus margin. */ - return (GeodesicTraceConfig){.coordinate_time_step = 0.1, - .max_steps = 65536, - .threshold = threshold}; + /* Per-backend integration policy. `default_step` is the initial DP54 trial + * step (and RK4's fixed step on the legacy comparison path), `default_budget` + * is the initial accepted-step resource allowance, `length_unit` scales the + * position tolerance and minimum step, and `default_max_step` bounds the DP54 + * trial step. `default_lookback` is the trusted coordinate-time coverage: it + * is an explicit constant or a geometry-derived physical time, never the + * product of the chosen step and the chosen step count. The directed + * worldtube crossing in Schwarzschild makes an escaping ray traverse the + * interior as a round trip, so its resource budget covers roughly twice the + * escape sphere plus margin. */ + double default_step; + unsigned int default_budget; + double length_unit; + double default_max_step; + double default_lookback; +#if defined(SPACETIME_SCHWARZSCHILD) + default_step = 0.1; + default_budget = 65536; + length_unit = 1.0; + /* The far-field cap should not force small steps after the adaptive error + * controller has resolved the strong-field segment. */ + default_max_step = 8.0; + /* Fixed-step physical coverage: 0.1 * 65536. */ + default_lookback = 6553.6; #elif defined(SPACETIME_ALCUBIERRE) - const double step = alcubierre_time_step(s); - const double budget = alcubierre_step_budget(s); - unsigned max_steps = ALCUBIERRE_MAX_TRACE_STEPS; - if (budget < (double)max_steps && isfinite(budget)) - max_steps = (unsigned)ceil(budget); - if (max_steps < 1024u) - max_steps = 1024u; - return (GeodesicTraceConfig){.coordinate_time_step = step, - .max_steps = max_steps, - .threshold = threshold}; + 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. */ + const double estimated_steps = alcubierre_step_budget(s); + unsigned int budget = ALCUBIERRE_MAX_TRACE_STEPS; + if (isfinite(estimated_steps) && estimated_steps < (double)budget) + budget = (unsigned int)ceil(estimated_steps); + if (budget < 1024u) + budget = 1024u; + default_budget = budget; + } + length_unit = s->alcubierre_radius; + default_max_step = default_step * 8.0; + /* Physical coordinate-time coverage from the bubble geometry and the + * asymptotic separation rate, independent of the chosen step or step count. */ + default_lookback = alcubierre_trace_time_budget(s); #else - return (GeodesicTraceConfig){.coordinate_time_step = 1.0, - .max_steps = 2048, - .threshold = threshold}; + default_step = 1.0; + default_budget = 2048; + length_unit = 1.0; + default_max_step = 16.0; + /* Fixed-step physical coverage: 1.0 * 2048. */ + default_lookback = 2048.0; #endif + + double step = default_step; + unsigned int budget = default_budget; + if (s->ode_initial_step_specified) + step = s->ode_initial_step; + if (s->trace_max_steps_specified) + budget = s->trace_max_steps; + + const double default_min_step = + fmax(1e-12 * length_unit, 16.0 * DBL_EPSILON * length_unit); + const double default_atol_x = 1e-9 * length_unit; + return (GeodesicTraceConfig){ + .coordinate_time_step = step, + .max_steps = budget, + .threshold = threshold, + .stepper = s->stepper, + .atol_x = s->ode_atol_x_specified ? s->ode_atol_x : default_atol_x, + .atol_Pi = s->ode_atol_pi_specified ? s->ode_atol_pi : 1e-9, + .atol_L = s->ode_atol_l_specified ? s->ode_atol_l : 1e-9, + .rtol = s->ode_rtol_specified ? s->ode_rtol : 1e-9, + .min_step = s->ode_min_step_specified ? s->ode_min_step : default_min_step, + .max_step = s->ode_max_step_specified ? s->ode_max_step : default_max_step, + .consecutive_rejection_limit = + s->ode_max_rejections_specified ? s->ode_max_rejections : 32u, + .max_lookback_time = s->trace_lookback_time_specified + ? s->trace_lookback_time + : default_lookback}; +} + +/* Resolve the retry resource policy. The flag-aware defaults keep an explicit + * zero (e.g. --retry-step-increment 0 to disable step retries) distinct from a + * value left unset, which the existing frame-level helper could not. */ +static RefinementConfig effective_refinement(const Settings *s, + const GeodesicTraceConfig *trace) { + RefinementConfig rc = s->refinement; + if (rc.retry_step_increment == 0 && !s->retry_step_increment_specified) + rc.retry_step_increment = trace->max_steps; + if (rc.max_total_steps == 0 && !s->max_total_steps_specified) + rc.max_total_steps = trace->max_steps > (UINT_MAX / 4u) + ? trace->max_steps + : trace->max_steps * 4u; + if (trace->stepper == GEODESIC_STEPPER_DP54) { + if (s->retry_lookback_increment_specified) { + rc.retry_lookback_increment = s->retry_lookback_increment; + } else if (rc.retry_lookback_increment == 0.0) { + rc.retry_lookback_increment = trace->max_lookback_time; + } + if (s->max_total_lookback_time_specified) { + rc.max_total_lookback_time = s->max_total_lookback_time; + } else if (rc.max_total_lookback_time == 0.0) { + rc.max_total_lookback_time = trace->max_lookback_time > DBL_MAX / 4.0 + ? DBL_MAX + : trace->max_lookback_time * 4.0; + } + } + return rc; +} + +/* Returns the first explicitly specified option that replay cannot honor; a + * render-only import must consume the stored policy instead of overriding it. */ +static const char *replay_tracing_option(const Settings *s) { + if (s->integrator_specified) return "--integrator"; + if (s->ode_rtol_specified) return "--ode-rtol"; + if (s->ode_atol_x_specified) return "--ode-atol-x"; + if (s->ode_atol_pi_specified) return "--ode-atol-pi"; + if (s->ode_atol_l_specified) return "--ode-atol-l"; + if (s->ode_initial_step_specified) return "--ode-initial-step"; + if (s->ode_min_step_specified) return "--ode-min-step"; + if (s->ode_max_step_specified) return "--ode-max-step"; + if (s->ode_max_rejections_specified) return "--ode-max-rejections"; + if (s->trace_max_steps_specified) return "--trace-max-steps"; + if (s->trace_lookback_time_specified) return "--trace-lookback-time"; + if (s->retry_step_increment_specified) return "--retry-step-increment"; + if (s->max_total_steps_specified) return "--max-total-steps"; + if (s->retry_lookback_increment_specified) return "--retry-lookback-increment"; + if (s->max_total_lookback_time_specified) return "--max-total-lookback-time"; + return NULL; +} + +/* Cross-option checks that need the resolved per-backend trace policy. */ +static int validate_settings(const Settings *s) { + const GeodesicTraceConfig trace = trace_config(s); + if (trace.stepper == GEODESIC_STEPPER_RK4) { + const char *bad = NULL; + if (s->ode_rtol_specified) bad = "--ode-rtol"; + else if (s->ode_atol_x_specified) bad = "--ode-atol-x"; + else if (s->ode_atol_pi_specified) bad = "--ode-atol-pi"; + else if (s->ode_atol_l_specified) bad = "--ode-atol-l"; + else if (s->ode_min_step_specified) bad = "--ode-min-step"; + else if (s->ode_max_step_specified) bad = "--ode-max-step"; + else if (s->ode_max_rejections_specified) bad = "--ode-max-rejections"; + else if (s->trace_lookback_time_specified) bad = "--trace-lookback-time"; + else if (s->retry_lookback_increment_specified) + bad = "--retry-lookback-increment"; + else if (s->max_total_lookback_time_specified) + bad = "--max-total-lookback-time"; + if (bad != NULL) { + fprintf(stderr, + "%s applies only to --integrator dp54; the fixed-step RK4 path " + "has no adaptive tolerance or lookback policy.\n", + bad); + return -1; + } + return 0; + } + if (!(trace.min_step <= trace.coordinate_time_step && + trace.coordinate_time_step <= trace.max_step)) { + fprintf(stderr, + "Adaptive step bounds must satisfy min <= initial <= max " + "(min=%.17g, initial=%.17g, max=%.17g).\n", + trace.min_step, trace.coordinate_time_step, trace.max_step); + return -1; + } + if (s->max_total_steps_specified && s->refinement.max_total_steps != 0 && + s->refinement.max_total_steps < trace.max_steps) { + fprintf(stderr, + "--max-total-steps (%u) must be >= the initial step budget (%u).\n", + s->refinement.max_total_steps, trace.max_steps); + return -1; + } + if (s->max_total_lookback_time_specified && + s->max_total_lookback_time < trace.max_lookback_time) { + fprintf(stderr, + "--max-total-lookback-time (%.17g) must be >= the initial lookback " + "budget (%.17g).\n", + s->max_total_lookback_time, trace.max_lookback_time); + return -1; + } + return 0; +} + +static const char *stepper_name(GeodesicStepper stepper) { + return stepper == GEODESIC_STEPPER_DP54 ? "dp54" : "rk4"; +} + +static void report_trace_config(const Settings *s, + const GeodesicTraceConfig *trace, + const char *label) { + if (!s->verbose) + return; + fprintf(stderr, + "%s: integrator=%s step=%.8g max_steps=%u threshold=%.8g " + "atol=(x %.8g, Pi %.8g, L %.8g) rtol=%.8g bounds=[%.8g, %.8g] " + "max_rejections=%u lookback=%.8g\n", + label, stepper_name(trace->stepper), trace->coordinate_time_step, + trace->max_steps, trace->threshold.value, trace->atol_x, + trace->atol_Pi, trace->atol_L, trace->rtol, trace->min_step, + trace->max_step, trace->consecutive_rejection_limit, + trace->max_lookback_time); } static int resolve_camera(Settings *s) { @@ -1098,6 +1462,8 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog, const ObserverState *observer, const char *output_path) { const GeodesicTraceConfig trace = trace_config(s); + const RefinementConfig refinement_config = effective_refinement(s, &trace); + report_trace_config(s, &trace, "Frame 0"); FrameLensMesh mesh = {0}; double *hdr = calloc((size_t)s->width * s->height * 3, sizeof *hdr); if (hdr == NULL || frame_lens_mesh_build_coarse(&mesh, s->width, s->height, @@ -1124,7 +1490,7 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog, s->refinement.max_level); const double refinement_start = omp_get_wtime(); if (frame_lens_mesh_refine_with_progress( - &mesh, spacetime, observer, &trace, &s->refinement, + &mesh, spacetime, observer, &trace, &refinement_config, s->verbose ? report_frame_refinement : NULL, (void *)s)) { frame_lens_mesh_destroy(&mesh); free(hdr); @@ -1137,11 +1503,13 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog, omp_get_wtime() - refinement_start, mesh.vertex_count, mesh.triangle_count); FrameBoundaryStats boundary_totals; + FrameLensMesh *frame_meshes[1] = {&mesh}; + report_boundary_stats(s, frame_meshes, 1, &refinement_config, + &boundary_totals); { - RefinementConfig boundary_config = s->refinement; - frame_retry_config_defaults(&boundary_config, &trace); - FrameLensMesh *meshes[1] = {&mesh}; - report_boundary_stats(s, meshes, 1, &boundary_config, &boundary_totals); + FrameTraceStats trace_stats; + frame_lens_mesh_trace_stats(&mesh, &trace_stats); + report_trace_stats(s, &trace_stats, "Frame 0"); } if (!boundary_allows_publish(s, &boundary_totals)) { frame_lens_mesh_destroy(&mesh); @@ -1230,8 +1598,7 @@ static int trace_movie_generation(Movie *movie, const Settings *s, RayPool rays = {0}; size_t ray_count = 0; size_t total_added = 0; - RefinementConfig effective = s->refinement; - frame_retry_config_defaults(&effective, trace); + RefinementConfig effective = effective_refinement(s, trace); for (size_t f = 0; f < movie->frame_count; ++f) { const int prepared = frame_lens_mesh_prepare_generation( &movie->frames[f].mesh, &effective); @@ -1252,12 +1619,14 @@ static int trace_movie_generation(Movie *movie, const Settings *s, if (fs->cached) continue; int rc; if (fs->kind == FRAME_SAMPLE_RETRY) { - const LensVertex *v = &fs->vertex; - rc = ray_pool_append_continuation( - &rays, f, sample, v->continuation_t, v->continuation_x, - v->continuation_Pi, v->continuation_log_alpha_p0, - v->continuation_log_alpha_p0_0, v->continuation_steps, - fs->step_limit); + GeodesicRayState state; + if (frame_vertex_continuation_state(&fs->vertex, &state)) { + ray_pool_destroy(&rays); + return -1; + } + rc = ray_pool_append_continuation_state(&rays, f, sample, &state, + fs->step_limit, + fs->lookback_limit); } else { rc = ray_pool_append(&rays, &movie->frames[f].observer, fs->vertex.camera_direction, f, sample); @@ -1454,6 +1823,7 @@ static int render_movie(const Settings *s, StarCatalog *catalog, ObserverTrack track = {0}; Movie movie = {0}; const GeodesicTraceConfig trace = trace_config(s); + report_trace_config(s, &trace, "Movie"); MovieOutputQueue output_queue; int output_queue_ready = 0; int result = -1; @@ -1478,8 +1848,7 @@ static int render_movie(const Settings *s, StarCatalog *catalog, break; } { - RefinementConfig boundary_config = s->refinement; - frame_retry_config_defaults(&boundary_config, &trace); + const RefinementConfig boundary_config = effective_refinement(s, &trace); FrameLensMesh **meshes = malloc(movie.frame_count * sizeof *meshes); if (meshes == NULL) goto done; @@ -1488,6 +1857,10 @@ static int render_movie(const Settings *s, StarCatalog *catalog, FrameBoundaryStats boundary_totals; report_boundary_stats(s, meshes, movie.frame_count, &boundary_config, &boundary_totals); + FrameTraceStats trace_totals = {0}; + for (size_t i = 0; i < movie.frame_count; ++i) + accumulate_trace_stats(&movie.frames[i].mesh, &trace_totals); + report_trace_stats(s, &trace_totals, "Movie"); free(meshes); if (!boundary_allows_publish(s, &boundary_totals)) goto done; @@ -1623,17 +1996,40 @@ done: } static int render_lens_map(const Settings *s, StarCatalog *catalog) { + const char *replay_conflict = replay_tracing_option(s); + if (replay_conflict != NULL) { + fprintf(stderr, + "%s cannot be combined with --lens-map-input; a render-only replay " + "consumes the integration and retry policy stored in the map.\n", + replay_conflict); + return -1; + } LensMap map = {0}; if (lens_map_read(s->lens_map_input_path, NULL, &map)) { fprintf(stderr, "Failed to read or validate lens map: %s\n", s->lens_map_input_path); return -1; } + if (s->verbose) + fprintf(stderr, + "Imported lens map v%u: integrator=%s step=%.8g max_steps=%u " + "lookback=%.8g retry_lookback=%.8g max_total_lookback=%.8g\n", + map.file_version, + stepper_name((GeodesicStepper)map.provenance.integrator), + map.provenance.coordinate_time_step, map.provenance.initial_max_steps, + map.provenance.max_lookback_time, + map.provenance.retry_lookback_increment, + map.provenance.max_total_lookback_time); + /* Render-only replay never retries, but the full source policy (including + * the coordinate-time retry quotas) is restored rather than replaced by the + * process's CLI defaults. */ RefinementConfig replay_config = { .max_level = map.provenance.max_level, .min_edge_pixels = map.provenance.min_edge_pixels, .min_area_pixels2 = map.provenance.min_area_pixels2, .retry_step_increment = map.provenance.retry_step_increment, - .max_total_steps = map.provenance.max_total_steps}; + .max_total_steps = map.provenance.max_total_steps, + .retry_lookback_increment = map.provenance.retry_lookback_increment, + .max_total_lookback_time = map.provenance.max_total_lookback_time}; int replay_incomplete = 0; /* Replay consumes stored decisions, not current CLI refinement defaults. */ for (size_t f = 0; f < map.frame_count; ++f) { @@ -1644,6 +2040,12 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) { lens_map_destroy(&map); return -1; } } + { + FrameTraceStats trace_totals = {0}; + for (size_t f = 0; f < map.frame_count; ++f) + accumulate_trace_stats(&map.frames[f].mesh, &trace_totals); + report_trace_stats(s, &trace_totals, "Imported map"); + } if ((s->width_specified && s->width != map.width) || (s->height_specified && s->height != map.height) || (s->fov_specified && fabs(s->horizontal_fov_deg - map.horizontal_fov_deg) > 1e-12)) { @@ -1878,6 +2280,12 @@ int main(int argc, char **argv) { "--refine-jacobian-min J " "--refine-min-edge-pixels P --refine-min-area-pixels2 A] " "[--draw-mesh] [--write-catalog PATH] " + "[--integrator rk4|dp54 --ode-rtol R --ode-atol-x A " + "--ode-atol-pi A --ode-atol-l A --ode-initial-step H " + "--ode-min-step H --ode-max-step H --ode-max-rejections N " + "--trace-max-steps N --trace-lookback-time T " + "--retry-step-increment N --max-total-steps N " + "--retry-lookback-increment T --max-total-lookback-time T] " "[--catalog-load-workers N] " "[--observer-track PATH --frames-dir DIR --frames-prefix NAME " "--start-time T --duration T --fps N | --movie-track-samples] " @@ -1989,9 +2397,16 @@ int main(int argc, char **argv) { "R > 0, sigma > 0.\n", stderr); return 1; } - if (alcubierre_step_budget(&settings) > - (double)ALCUBIERRE_MAX_TRACE_STEPS) { - /* The budget has a V-shaped minimum at sigma = 0.5, where the step + /* 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. */ + if (settings.stepper == GEODESIC_STEPPER_RK4 && + 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. */ @@ -2001,9 +2416,9 @@ int main(int argc, char **argv) { else if (settings.alcubierre_sigma < 0.5) sigma_advice = "increase --alcubierre-sigma, "; fprintf(stderr, - "Alcubierre trace budget exceeds the %u-step cap; decrease " - "--alcubierre-radius, %sor move --alcubierre-vs away from " - "+/-1.\n", + "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); spacetime_destroy(&spacetime); return 2; diff --git a/src/ray.c b/src/ray.c index 6241640..520ff8f 100644 --- a/src/ray.c +++ b/src/ray.c @@ -16,7 +16,10 @@ int ray_pool_init(RayPool *p, size_t capacity) { RAY_ALLOC(direction0) && RAY_ALLOC(direction1) && RAY_ALLOC(direction2) && RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(log_alpha_p0_0) && RAY_ALLOC(activate_t) && RAY_ALLOC(steps) && - RAY_ALLOC(step_limit) && RAY_ALLOC(continuation) && + RAY_ALLOC(step_limit) && RAY_ALLOC(integration_start_time) && + RAY_ALLOC(next_step) && RAY_ALLOC(rejected_steps) && + RAY_ALLOC(rhs_evaluations) && RAY_ALLOC(previous_rejected) && + RAY_ALLOC(lookback_limit) && RAY_ALLOC(continuation) && RAY_ALLOC(frame_id) && RAY_ALLOC(vertex_id) && RAY_ALLOC(status) && RAY_ALLOC(endpoint))) { ray_pool_destroy(p); @@ -55,30 +58,45 @@ int ray_pool_append(RayPool *p, const ObserverState *observer, .final_log_alpha_p0_0 = NAN, .threshold_value = NAN}; p->step_limit[i] = 0; + /* Adaptive control state starts unset; ray_pool_preroute() seeds the window + * start from the resolved activation time. Initializing here as well keeps + * a directly advanced pool slot well-defined. */ + p->integration_start_time[i] = observer->coordinate_time; + p->next_step[i] = 0.0; + p->rejected_steps[i] = 0; + p->rhs_evaluations[i] = 0; + p->previous_rejected[i] = 0; + p->lookback_limit[i] = 0.0; p->continuation[i] = 0; p->log_alpha_p0_0[i] = 0.0; ++p->count; return 0; } -int ray_pool_append_continuation(RayPool *p, size_t frame_id, - size_t vertex_id, double t, const double x[3], - const double Pi[3], double log_alpha_p0, - double log_alpha_p0_0, unsigned int steps, - unsigned int limit) { - if (p == NULL || p->count == p->capacity || x == NULL || Pi == NULL) +int ray_pool_append_continuation_state(RayPool *p, size_t frame_id, + size_t vertex_id, + const GeodesicRayState *state, + unsigned int accepted_limit, + double lookback_limit) { + if (p == NULL || p->count == p->capacity || state == NULL) return -1; const size_t i = p->count; - p->t[i] = t; - p->activate_t[i] = t; + p->t[i] = state->coordinate_time; + p->activate_t[i] = state->coordinate_time; p->observer[i] = NULL; p->direction0[i] = p->direction1[i] = p->direction2[i] = 0.0; - p->x0[i] = x[0]; p->x1[i] = x[1]; p->x2[i] = x[2]; - p->p0[i] = Pi[0]; p->p1[i] = Pi[1]; p->p2[i] = Pi[2]; - p->log_alpha_p0[i] = log_alpha_p0; - p->log_alpha_p0_0[i] = log_alpha_p0_0; - p->steps[i] = steps; - p->step_limit[i] = limit; + p->x0[i] = state->x[0]; p->x1[i] = state->x[1]; p->x2[i] = state->x[2]; + p->p0[i] = state->Pi[0]; p->p1[i] = state->Pi[1]; p->p2[i] = state->Pi[2]; + p->log_alpha_p0[i] = state->log_alpha_p0; + p->log_alpha_p0_0[i] = state->log_alpha_p0_0; + p->steps[i] = state->steps; + p->step_limit[i] = accepted_limit; + p->integration_start_time[i] = state->integration_start_time; + p->next_step[i] = state->next_step; + p->rejected_steps[i] = state->rejected_steps; + p->rhs_evaluations[i] = state->rhs_evaluations; + p->previous_rejected[i] = state->previous_rejected; + p->lookback_limit[i] = lookback_limit; p->continuation[i] = 1; p->frame_id[i] = frame_id; p->vertex_id[i] = vertex_id; @@ -88,16 +106,34 @@ int ray_pool_append_continuation(RayPool *p, size_t frame_id, .outcome = RAY_OUTCOME_INCOMPLETE, .reason = RAY_REASON_NONE, .stop_coordinate_time = NAN, - .accepted_steps = steps, + .accepted_steps = state->steps, + .accepted_step_limit = accepted_limit, .final_x = {NAN, NAN, NAN}, .final_Pi = {NAN, NAN, NAN}, .final_log_alpha_p0 = NAN, - .final_log_alpha_p0_0 = log_alpha_p0_0, + .final_log_alpha_p0_0 = state->log_alpha_p0_0, .threshold_value = NAN}; ++p->count; return 0; } +int ray_pool_append_continuation(RayPool *p, size_t frame_id, + size_t vertex_id, double t, const double x[3], + const double Pi[3], double log_alpha_p0, + double log_alpha_p0_0, unsigned int steps, + unsigned int limit) { + if (x == NULL || Pi == NULL) + return -1; + const GeodesicRayState state = {.coordinate_time = t, + .x = {x[0], x[1], x[2]}, + .Pi = {Pi[0], Pi[1], Pi[2]}, + .log_alpha_p0 = log_alpha_p0, + .log_alpha_p0_0 = log_alpha_p0_0, + .steps = steps}; + return ray_pool_append_continuation_state(p, frame_id, vertex_id, &state, + limit, 0.0); +} + void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) { if (p == NULL || source == NULL) return; @@ -151,6 +187,14 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) { continue; } p->activate_t[i] = route.activate_t; + /* The adaptive window starts at the resolved activation/entry state; the + * first trial step and all cost counters are freshly initialized. An + * integration start time of zero is a legal coordinate time. */ + p->integration_start_time[i] = route.activate_t; + p->next_step[i] = 0.0; + p->rejected_steps[i] = 0; + p->rhs_evaluations[i] = 0; + p->previous_rejected[i] = 0; p->x0[i] = route.x[0]; p->x1[i] = route.x[1]; p->x2[i] = route.x[2]; @@ -169,7 +213,10 @@ void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) { p->activate_t[i] <= slab->t_lo) continue; p->t[i] = p->activate_t[i]; - /* Continuation rays keep the accepted-step count they already consumed. */ + /* Continuation rays keep the accepted-step count they already consumed. + * Activation never resets adaptive control state; for a new ray it was + * already seeded by preroute, for a continuation it comes from the + * installed resume state. */ if (!p->continuation[i]) p->steps[i] = 0; p->status[i] = RAY_POOL_ACTIVE; @@ -191,10 +238,17 @@ void ray_pool_advance_active(RayPool *p, const MetricSlab *slab, .Pi = {p->p0[i], p->p1[i], p->p2[i]}, .log_alpha_p0 = p->log_alpha_p0[i], .log_alpha_p0_0 = p->log_alpha_p0_0[i], - .steps = p->steps[i]}; + .steps = p->steps[i], + .integration_start_time = p->integration_start_time[i], + .next_step = p->next_step[i], + .rejected_steps = p->rejected_steps[i], + .rhs_evaluations = p->rhs_evaluations[i], + .previous_rejected = p->previous_rejected[i]}; GeodesicTraceConfig per_ray = *config; if (p->step_limit[i] != 0) per_ray.max_steps = p->step_limit[i]; + if (p->lookback_limit[i] != 0.0) + per_ray.max_lookback_time = p->lookback_limit[i]; const GeodesicAdvanceResult result = geodesic_advance_past_ray(slab, &s, slab->t_lo, &per_ray, &p->endpoint[i]); p->t[i] = s.coordinate_time; @@ -202,6 +256,11 @@ void ray_pool_advance_active(RayPool *p, const MetricSlab *slab, p->p0[i] = s.Pi[0]; p->p1[i] = s.Pi[1]; p->p2[i] = s.Pi[2]; p->log_alpha_p0[i] = s.log_alpha_p0; p->steps[i] = s.steps; + p->integration_start_time[i] = s.integration_start_time; + p->next_step[i] = s.next_step; + p->rejected_steps[i] = s.rejected_steps; + p->rhs_evaluations[i] = s.rhs_evaluations; + p->previous_rejected[i] = s.previous_rejected; if (result == GEODESIC_ADVANCE_TERMINATED) p->status[i] = p->endpoint[i].outcome == RAY_OUTCOME_UNRESOLVED ? RAY_POOL_UNRESOLVED @@ -228,6 +287,9 @@ void ray_pool_destroy(RayPool *p) { free(p->p0); free(p->p1); free(p->p2); free(p->log_alpha_p0); free(p->log_alpha_p0_0); free(p->activate_t); free(p->steps); free(p->step_limit); + free(p->integration_start_time); free(p->next_step); + free(p->rejected_steps); free(p->rhs_evaluations); + free(p->previous_rejected); free(p->lookback_limit); free(p->continuation); free(p->frame_id); free(p->vertex_id); free(p->status); free(p->endpoint); diff --git a/src/ray.h b/src/ray.h index f7439f6..3079261 100644 --- a/src/ray.h +++ b/src/ray.h @@ -25,6 +25,20 @@ typedef struct { unsigned int *steps; /* Per-ray total accepted-step limit; zero means use the trace config. */ unsigned int *step_limit; + /* Adaptive (DP54) per-ray control state, copied verbatim from the resume + * state and never reset by activation. All fields are ignored by the legacy + * RK4 path. `integration_start_time` is the activation/entry time of the + * adaptive window (not the camera energy reference), `next_step` is a + * positive trial-step magnitude (zero means "derive from the config"), + * `rejected_steps`/`rhs_evaluations` are cumulative cost and + * `previous_rejected` flags a rejected last trial. */ + double *integration_start_time; + double *next_step; + unsigned int *rejected_steps; + unsigned long *rhs_evaluations; + unsigned int *previous_rejected; + /* Per-ray total lookback time budget; zero means use the trace config. */ + double *lookback_limit; /* Nonzero for a retry that resumes from a saved state instead of from the * camera; such rays are not pre-routed. */ uint8_t *continuation; @@ -39,7 +53,19 @@ int ray_pool_append(RayPool *pool, const ObserverState *observer, const double direction[3], size_t frame_id, size_t vertex_id); /* Append a retry that resumes an UNRESOLVED ray from its last accepted state. - * `limit` is the new total accepted-step budget for this ray. */ + * Every continuous and adaptive control field of `state` is copied so the + * resumed ray never replays from the camera, resets L0, or forgets its + * accumulated rejection/RHS counters. `accepted_limit` is the new total + * accepted-step budget; `lookback_limit` is the new total coordinate-time + * lookback budget (zero means use the trace config). Neither quota is part of + * the continuous state. */ +int ray_pool_append_continuation_state(RayPool *pool, size_t frame_id, + size_t vertex_id, + const GeodesicRayState *state, + unsigned int accepted_limit, + double lookback_limit); +/* Legacy scattered-field wrapper around the state interface above. It builds + * an RK4-compatible state (zero adaptive control) and a zero lookback quota. */ int ray_pool_append_continuation(RayPool *pool, size_t frame_id, size_t vertex_id, double t, const double x[3], const double Pi[3], double log_alpha_p0, diff --git a/tests/test_adaptive_cli.py b/tests/test_adaptive_cli.py new file mode 100644 index 0000000..c40718d --- /dev/null +++ b/tests/test_adaptive_cli.py @@ -0,0 +1,545 @@ +#!/usr/bin/env python3 +"""Exercise the production DP54 default, v3 lens-map provenance/replay and the +adaptive retry/budget policy from the CLI. + +The production default is now adaptive Dormand-Prince 5(4). These checks +require that a run without --integrator exports wire code 1 and is physically +and bit-for-bit equal to an explicit --integrator dp54 run, so `make test` +truly covers the production default rather than only the explicit path. +--integrator rk4 stays available for the legacy wire code and for a +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 os +import re +import struct +import subprocess +import sys +import tempfile +import zlib +from pathlib import Path + +# Keep scratch data inside the pre-approved OpenCode scratch directory instead +# of creating directories directly under the system temporary root. +TMP_ROOT = Path('/tmp/opencode') +TMP_ROOT.mkdir(parents=True, exist_ok=True) + +BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve() +TESTDIR = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else BUILD +ENV = dict(os.environ, OMP_NUM_THREADS='4') + +# v3 wire offsets (see src/lens_map.c). The header is deliberately not part of +# the payload CRC. +VERSION_OFFSET = 8 +FRAME_COUNT_OFFSET = 32 +PROVENANCE_OFFSET = 40 +PROVENANCE_V3_OFFSET = 100 +VERTEX_COUNT_OFFSET = 200 +TRIANGLE_COUNT_OFFSET = 208 +VERTEX_START = 224 +VERTEX_SIZE = 108 +TRIANGLE_SIZE = 32 + +ATOL_FIELDS = ('atol_x', 'atol_Pi', 'atol_L', 'rtol', 'min_step', 'max_step', + 'max_lookback_time', 'retry_lookback_increment', + 'max_total_lookback_time') +# Machine-roundoff floor for comparing two adaptive integrations; below this a +# difference carries no convergence information. +ROUNDOFF_FLOOR = 1e-12 +ENDPOINT_ASSERT = 1e-6 + + +def run(binary, *args, ok=True, env=ENV): + result = subprocess.run([str(binary), *map(str, args)], env=env, + capture_output=True, text=True) + if (result.returncode == 0) != ok: + raise AssertionError( + f'{binary.name} {args}: rc={result.returncode}\n{result.stderr}') + return result + + +def image_payload(path): + data = path.read_bytes() + assert data[:8] == b'\x89PNG\r\n\x1a\n', f'not a PNG: {path}' + offset, compressed = 8, bytearray() + while offset < len(data): + count, kind = struct.unpack_from('>I4s', data, offset) + payload = data[offset + 8:offset + 8 + count] + if kind == b'IDAT': + compressed.extend(payload) + offset += count + 12 + return bytes(zlib.decompress(compressed)) + + +def map_provenance(path): + data = path.read_bytes() + assert data[:8] == b'GRLENS\x01\x00' + version = struct.unpack_from(' 0 and dflt_prov['rtol'] > 0 + assert dflt_prov['max_lookback_time'] > 0 + assert dflt_prov['max_consecutive_rejections'] > 0 + expl_map = tmp / f'{backend}_expl.grlens' + expl_out, _ = single('expl', *hdr_args, '--integrator', 'dp54', + '--lens-map-output', expl_map) + assert dflt_map.read_bytes() == expl_map.read_bytes(), \ + 'default map differs from explicit dp54' + assert image_payload(dflt_out) == image_payload(expl_out) + if hdr_available: + dflt_hdr = dflt_out.with_name(dflt_out.stem + '_HDR.fits') + expl_hdr = expl_out.with_name(expl_out.stem + '_HDR.fits') + assert dflt_hdr.read_bytes() == expl_hdr.read_bytes() + dflt_vertices, _ = map_vertices(dflt_map) + + # 2) The legacy RK4 wire code must be 0 and its cost counters must be + # real (nonzero RHS evaluations), not legacy zeros. + rk4_map = tmp / f'{backend}_rk4.grlens' + single('rk4', '--integrator', 'rk4', '--lens-map-output', rk4_map) + _, _, rk4_prov = map_provenance(rk4_map) + assert rk4_prov['integrator'] == 0, rk4_prov + rk4_vertices, _ = map_vertices(rk4_map) + assert sum_rhs(rk4_vertices) > 0, 'RK4 RHS cost counters are not real' + + # 3) Same-camera tolerance convergence with three levels. Outcome, + # reason and end must not change between levels, and the deviation + # from the tightest reference must shrink as the tolerance tightens. + # These are local ODE tolerances, not a global sky-error bound. + tol_maps = {} + for tol in ('1e-7', '1e-9', '1e-12'): + m = tmp / f'{backend}_tol_{tol}.grlens' + single(f'tol_{tol}', '--integrator', 'dp54', '--ode-rtol', tol, + '--ode-atol-x', tol, '--ode-atol-pi', tol, + '--ode-atol-l', tol, '--lens-map-output', m) + tol_maps[tol] = map_vertices(m)[0] + mism7, err7 = endpoint_deviation(tol_maps['1e-7'], tol_maps['1e-12']) + mism9, err9 = endpoint_deviation(tol_maps['1e-9'], tol_maps['1e-12']) + assert mism7 == 0 and mism9 == 0, \ + f'{backend}: tolerance levels disagree on outcome/end' + assert err9 < ENDPOINT_ASSERT, (backend, 'default vs tight', err9) + assert err7 + ROUNDOFF_FLOOR >= err9, \ + f'{backend}: tightening tolerance did not reduce error ' \ + f'({err7} -> {err9})' + print(f'{backend}: default==dp54, tol errors 1e-7={err7:.3g} ' + f'1e-9={err9:.3g}', flush=True) + + # 4) Render-only replay of the default map must be bit-identical and + # its stored statistics must equal the live trace cost. + replay_out = tmp / f'{backend}_replay.{ext}' + replay_run = run(binary, *common, *hdr_args, '--lens-map-input', + dflt_map, '--verbose', '--output', replay_out) + assert image_payload(replay_out) == image_payload(dflt_out) + if hdr_available: + replay_hdr = replay_out.with_name(replay_out.stem + '_HDR.fits') + assert dflt_out.with_name(dflt_out.stem + '_HDR.fits').read_bytes() \ + == replay_hdr.read_bytes() + live_cost = trace_cost(dflt_run.stderr, 'Frame 0') + replay_cost = trace_cost(replay_run.stderr, 'Imported map') + assert live_cost is not None and replay_cost is not None + assert live_cost == replay_cost, (live_cost, replay_cost) + + # 5) Explicit zeros in the retry policy must survive, not be filled in + # by the derived defaults. + zero_step_map = tmp / f'{backend}_zero_step.grlens' + single('zero_step', '--integrator', 'dp54', '--retry-step-increment', + '0', '--lens-map-output', zero_step_map) + _, _, zero_step = map_provenance(zero_step_map) + assert zero_step['retry_step_increment'] == 0, zero_step + assert zero_step['max_total_steps'] > 0, zero_step + zero_time_map = tmp / f'{backend}_zero_time.grlens' + single('zero_time', '--integrator', 'dp54', + '--retry-lookback-increment', '0', '--lens-map-output', + zero_time_map) + _, _, zero_time = map_provenance(zero_time_map) + assert zero_time['retry_lookback_increment'] == 0, zero_time + assert zero_time['max_total_lookback_time'] >= \ + zero_time['max_lookback_time'], zero_time + + # 6) RK4 rejects every DP-only option rather than silently ignoring it. + rk4_errors = [ + (['--integrator', 'rk4', '--ode-rtol', 1e-9], 'applies only'), + (['--integrator', 'rk4', '--ode-min-step', 1e-9], 'applies only'), + (['--integrator', 'rk4', '--ode-max-step', 1e-3], 'applies only'), + (['--integrator', 'rk4', '--ode-max-rejections', 4], 'applies only'), + (['--integrator', 'rk4', '--trace-lookback-time', 1], 'applies only'), + (['--integrator', 'rk4', '--retry-lookback-increment', 1], 'applies only'), + (['--integrator', 'rk4', '--max-total-lookback-time', 2], 'applies only'), + ] + # DP cross-field validation and malformed values fail immediately. + invalid = [ + (['--integrator', 'bogus'], None), + (['--integrator'], None), + (['--integrator', 'dp54', '--ode-rtol', 0], None), + (['--integrator', 'dp54', '--ode-rtol', -1], None), + (['--integrator', 'dp54', '--ode-atol-x', 'nan'], None), + (['--integrator', 'dp54', '--ode-max-rejections', 0], None), + (['--integrator', 'dp54', '--trace-max-steps', 0], None), + (['--integrator', 'dp54', '--ode-initial-step', 5, + '--ode-max-step', 1], 'min <= initial <= max'), + (['--integrator', 'dp54', '--ode-min-step', 4, + '--ode-initial-step', 2], 'min <= initial <= max'), + (['--integrator', 'dp54', '--trace-lookback-time', 0], None), + (['--integrator', 'dp54', '--retry-step-increment', -1], None), + ] + for options, message in rk4_errors + invalid: + missing = tmp / 'adaptive_should_not_exist.csv' + result = run(binary, '--catalog', missing, *options, ok=False) + if message: + assert message in result.stderr, (options, result.stderr) + assert not missing.exists(), result.stderr + + # 7) Replay must consume the stored policy: explicit tracing options + # cannot be layered on top of --lens-map-input. + conflict = run(binary, *common, '--lens-map-input', dflt_map, + '--integrator', 'rk4', '--output', + tmp / 'conflict.png', ok=False) + assert 'cannot be combined with --lens-map-input' in conflict.stderr, \ + conflict.stderr + conflict2 = run(binary, *common, '--lens-map-input', dflt_map, + '--trace-max-steps', 100, '--output', + tmp / 'conflict2.png', ok=False) + assert 'cannot be combined with --lens-map-input' in conflict2.stderr, \ + conflict2.stderr + conflict3 = run(binary, *common, '--lens-map-input', dflt_map, + '--integrator', 'dp54', '--ode-rtol', 1e-9, + '--output', tmp / 'conflict3.png', ok=False) + assert 'cannot be combined with --lens-map-input' in conflict3.stderr, \ + conflict3.stderr + + # 8) Single vs movie: the same physical observer event must agree, and + # threads/slabs must not change the stored adaptive endpoints. + track = tmp / f'{backend}.csv' + observer_test = TESTDIR / f'test_observer_{backend}' + if observer_test.exists(): + run(observer_test, track) + moving_single = tmp / f'{backend}_moving.grlens' + single('moving', '--observer-position', 3, -4, 5, + '--observer-velocity', 0.2, -0.1, 0.3, + '--look-ra-deg', 37, '--look-dec-deg', -23, + '--camera-roll-deg', 19, '--lens-map-output', moving_single) + movie_map = tmp / f'{backend}_movie.grlens' + run(binary, *common, '--observer-track', track, '--frames-dir', tmp, + '--frames-prefix', f'{backend}_movie', '--duration', 0, + '--fps', 1, '--lens-map-output', movie_map) + single_v, _ = map_vertices(moving_single) + movie_v, _ = map_vertices(movie_map) + mism, dev = endpoint_deviation(single_v, movie_v) + assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev) + + thread_maps = [] + for threads in (1, 2, 4): + m = tmp / f'{backend}_movie_{threads}thr.grlens' + run(binary, *common, '--observer-track', track, '--frames-dir', + tmp, '--frames-prefix', f'{backend}_m{threads}', + '--duration', 0, '--fps', 1, '--lens-map-output', m, + env=dict(ENV, OMP_NUM_THREADS=str(threads))) + thread_maps.append(m) + reference = thread_maps[0].read_bytes() + for m in thread_maps[1:]: + assert m.read_bytes() == reference, \ + f'{backend}: DP movie map changed across threads' + + slab_maps = [] + for slab in (2, 8, 64): + m = tmp / f'{backend}_slab_{slab}.grlens' + run(binary, *common, '--observer-track', track, '--frames-dir', + tmp, '--frames-prefix', f'{backend}_s{slab}', + '--slab-duration', slab, '--duration', 0, '--fps', 1, + '--lens-map-output', m) + slab_maps.append(m) + base_v, _ = map_vertices(slab_maps[0]) + for m in slab_maps[1:]: + other_v, _ = map_vertices(m) + mism, dev = endpoint_deviation(base_v, other_v) + assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev) + print(f'{backend}: DP movie threads/slabs agree', flush=True) + + # 9) Budget: a tiny initial coordinate-time budget leaves UNRESOLVED + # rays; with no room to grow the publication gate refuses the frame, + # while retry increments that can grow resolve it. + if backend == 'schwarzschild': + refused = tmp / f'{backend}_refused.{ext}' + small = ['--integrator', 'dp54', '--trace-lookback-time', 1e-6, + '--retry-lookback-increment', 0, + '--max-total-lookback-time', 1e-6] + result = run(binary, *common, '--output', refused, *small, ok=False) + assert 'Incomplete render refused' in result.stderr, result.stderr + assert not refused.exists() + allow = tmp / f'{backend}_allow.{ext}' + run(binary, *common, '--output', allow, '--allow-incomplete', *small) + assert allow.exists() + + direct = tmp / f'{backend}_direct.{ext}' + direct_result = subprocess.run( + [str(binary), *map(str, common), '--output', str(direct), + '--integrator', 'dp54', '--trace-lookback-time', '4000'], + env=ENV, capture_output=True, text=True) + if direct_result.returncode == 0: + retried = tmp / f'{backend}_retried.{ext}' + run(binary, *common, '--output', retried, '--integrator', 'dp54', + '--trace-lookback-time', 1e-6, + '--retry-lookback-increment', 25, + '--max-total-lookback-time', 4000) + assert image_payload(retried) + print('schwarzschild: retry budget resolves previously ' + 'unresolved rays', flush=True) + else: + print('schwarzschild: direct budget scene still unresolved; ' + 'retry-resolution case skipped', flush=True) + + # 10) Fixed-step reference convergence on a small scene. The + # accepted budget is explicit and large so the finer step does + # not silently shorten the traced history. Both fixed-step + # levels and the DP54 default must agree below ENDPOINT_ASSERT. + # If the reference itself does not converge this must FAIL and + # be reported, never loosened into a false zero. + ref_common = ['--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] + + def ref_map(tag, *options): + m = tmp / f'{backend}_ref_{tag}.grlens' + run(binary, *ref_common, '--output', + tmp / f'{backend}_ref_{tag}.{ext}', '--lens-map-output', m, + *options) + return map_vertices(m)[0] + + rk4_04 = ref_map('rk4_04', '--integrator', 'rk4', + '--ode-initial-step', 0.04, '--trace-max-steps', + '262144') + rk4_02 = ref_map('rk4_02', '--integrator', 'rk4', + '--ode-initial-step', 0.02, '--trace-max-steps', + '262144') + dp_tight = ref_map('dp_tight', '--ode-rtol', '1e-12', + '--ode-atol-x', '1e-12', '--ode-atol-pi', + '1e-12', '--ode-atol-l', '1e-12') + assert sum(1 for v in rk4_02 if v[10] == 0) > 0, \ + 'reference scene has no escaped ray' + mism, ref_dev = endpoint_deviation(rk4_04, rk4_02) + assert mism == 0, 'RK4 reference levels disagree on outcome/end' + assert ref_dev < ENDPOINT_ASSERT, \ + f'RK4 reference not converged: {ref_dev}; report to parent' + mism, dp_dev = endpoint_deviation(dp_tight, rk4_02) + assert mism == 0, 'DP54 default disagrees with RK4 reference' + assert dp_dev < ENDPOINT_ASSERT, (dp_dev,) + print(f'schwarzschild: RK4 ref convergence {ref_dev:.3g}, ' + f'DP default vs fine RK4 {dp_dev:.3g}', flush=True) + + 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. + 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) + + # 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_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] + + 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 + + # 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' + 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) + _, _, 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' + + # 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 + + print('alcubierre: default DP config validated and produced escapes; ' + 'extreme DP quota path ok, RK4 guard still rejects', flush=True) diff --git a/tests/test_asymptotic.c b/tests/test_asymptotic.c index 44d85a5..3e1bea4 100644 --- a/tests/test_asymptotic.c +++ b/tests/test_asymptotic.c @@ -307,10 +307,11 @@ static void test_boundary_semantics_minkowski(void) { spacetime_destroy(&source); } -static void test_boundary_semantics_generic(void) { - /* velocity_constant == 0 forces the generic bracketed driver. A finite - * history bounds the outward/tangent searches, which must not be reported - * as entries (they end as TIME_RANGE_EXHAUSTED instead). */ +static void test_nonconstant_route_unsupported(void) { + /* velocity_constant == 0 signals an arbitrary accelerated worldtube. The + * camera-inside containment test still runs first; once the ray starts + * outside, the non-constant route is refused explicitly instead of being + * searched with a coarse bracket. */ SyntheticContext context = {.radius = 10.0, .valid_t_min = -100.0, .constant = 0}; @@ -321,15 +322,15 @@ static void test_boundary_semantics_generic(void) { &source, &on_boundary, (double[]){-1.0, 0.0, 0.0}, &route); CHECK(inward == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_INSIDE, - "generic on-boundary inward is inside"); + "nonconstant on-boundary inward is still inside"); const AsymptoticStatus outward = asymptotic_route_camera( &source, &on_boundary, (double[]){1.0, 0.0, 0.0}, &route); - CHECK(outward == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, - "generic on-boundary outward is not an entry"); + CHECK(outward == ASYMPTOTIC_UNSUPPORTED, + "nonconstant on-boundary outward is unsupported"); const AsymptoticStatus tangent = asymptotic_route_camera( &source, &on_boundary, (double[]){0.0, 1.0, 0.0}, &route); - CHECK(tangent == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, - "generic on-boundary tangent is not an entry"); + CHECK(tangent == ASYMPTOTIC_UNSUPPORTED, + "nonconstant on-boundary tangent is unsupported"); } static void test_negative_radius_root_guard(void) { @@ -525,11 +526,12 @@ static void test_interior_crossing_bisection_failure(void) { "interior crossing bisection propagates history exhaustion"); } -static void test_generic_bisection_failure(void) { - /* The isolated invalid window lands on a bisection midpoint while the - * bracket endpoints stay valid, so only the bisection can see it. With the - * strict F < 0 entry test, the bracket is s = 80 (F == 0) to s = 90 - * (F < 0), so the first midpoint is t = -85. */ +static void test_nonconstant_preroute_unsupported(void) { + /* The accelerated fixture is refused explicitly at the route entry; its + * former coarse bracketed search (and the bisection-midpoint history-hole + * case it exercised) is replaced by the unsupported contract. The history + * reason itself stays covered by the constant and piecewise-constant + * worldtube tests below. */ SyntheticContext context = {.radius = 20.0, .valid_t_min = -1.0e30, .constant = 0, @@ -538,9 +540,26 @@ static void test_generic_bisection_failure(void) { SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; + CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, + &route) == ASYMPTOTIC_UNSUPPORTED, + "nonconstant worldtube route is unsupported"); +} + +static void test_piecewise_constant_history_hole(void) { + /* Piecewise-constant motion stays supported, including its history + * contract: when the segment walk steps past the valid history, the route + * reports TIME_RANGE_EXHAUSTED and never disguises it as a miss. */ + SyntheticContext context = {.radius = 10.0, + .valid_t_min = -3.0, + .constant = 1, + .segment_t = -5.0, + .has_segment = 1}; + SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; + const ObserverState camera = flat_observer(50.0, 0.0, 0.0); + AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, - "generic worldtube bisection propagates sample failure"); + "piecewise-constant segment history hole is exhausted"); } static void test_interior_history_exhaustion(void) { @@ -641,36 +660,30 @@ static void test_moving_sphere(void) { "co-moving ray misses"); } -static void test_accelerated_worldtube(void) { +static void test_accelerated_worldtube_unsupported(void) { + /* A genuinely accelerating (non-constant velocity) worldtube has no strict + * relative-motion interval bound, so the route is explicitly unsupported. + * Its former finite-history branch is covered by the constant and + * piecewise-constant history tests instead. */ SyntheticContext context = {.vx = 0.0, .accel = 0.02, .radius = 20.0, .valid_t_min = -1.0e30, .constant = 0}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; const ObserverState camera = flat_observer(100.0, 0.0, 0.0); AsymptoticRoute route; CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, - &route) == ASYMPTOTIC_OK && - route.kind == ASYMPTOTIC_ROUTE_ENTRY, - "accelerated worldtube entry"); - double value; - CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t, - route.x, &value) == 0 && - fabs(value) <= 1e-13 * 20.0 * 20.0, - "accelerated entry lies on worldtube"); - CHECK(route.activate_t < -40.0 && route.activate_t > -60.0, - "accelerated entry time in range"); - - context.valid_t_min = -30.0; - CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, - &route) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED, - "exhausted history is not a miss"); + &route) == ASYMPTOTIC_UNSUPPORTED, + "accelerated worldtube route is unsupported"); } -static void test_accelerated_segment(void) { +static void test_piecewise_segment_entry(void) { + /* The two-segment motion is piecewise constant, so velocity_constant == 1 + * per segment and the closed quadratic path must still find the entry in + * the second segment (the analytic segmented first-entry case). */ SyntheticContext context = {.vx = 0.0, .accel = 0.0, .radius = 20.0, .valid_t_min = -1.0e30, - .constant = 0, + .constant = 1, .segment_t = -50.0, .has_segment = 1}; SpacetimeSource source = {.ops = &synthetic_ops, .context = &context}; @@ -679,7 +692,7 @@ static void test_accelerated_segment(void) { CHECK(asymptotic_route_camera(&source, &camera, (double[]){-1.0, 0.0, 0.0}, &route) == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY, - "cross-segment entry"); + "piecewise cross-segment entry"); CHECK(route.activate_t < context.segment_t, "entry lies past the motion-segment boundary"); CHECK(fabs(route.activate_t + 65.0) < 1e-6, "cross-segment entry time"); @@ -731,16 +744,17 @@ int main(void) { test_large_radius_quadratic(); test_round_trip(); test_moving_sphere(); - test_accelerated_worldtube(); - test_accelerated_segment(); + test_accelerated_worldtube_unsupported(); + test_piecewise_segment_entry(); test_boundary_semantics_minkowski(); - test_boundary_semantics_generic(); + test_nonconstant_route_unsupported(); test_negative_radius_root_guard(); test_source_finalize(); test_motion_segment_domain(); test_schwarzschild_sample_failures(); test_end_protocol_error(); - test_generic_bisection_failure(); + test_nonconstant_preroute_unsupported(); + test_piecewise_constant_history_hole(); test_interior_history_exhaustion(); test_interior_crossing_bisection_failure(); test_ray_pool_lifecycle(); diff --git a/tests/test_camera_cli.py b/tests/test_camera_cli.py index f5dde69..85af5ad 100644 --- a/tests/test_camera_cli.py +++ b/tests/test_camera_cli.py @@ -88,19 +88,31 @@ def image_payload(path, dimensions=(64, 48), allow_black=False): return raw +# Version 3 wire layout. The first 100 bytes are the v2 provenance; the v3 +# adaptive policy appends 9 doubles and a u32 (76 bytes) so the provenance +# block ends at 176. Each frame header is 48 bytes, so vertex payload starts at +# 224. A v3 vertex keeps the 84-byte v2 record and appends three u64 cost +# counters; a triangle stays 32 bytes. The CRC covers only the vertex+triangle +# payload, never the header. +MAP_PROVENANCE_END = 176 +MAP_FRAME_HEADER_START = MAP_PROVENANCE_END +MAP_VERTEX_START = MAP_PROVENANCE_END + 48 +MAP_VERTEX_SIZE = 84 + 24 +MAP_TRIANGLE_SIZE = 32 + + def map_vertices(path): data = path.read_bytes() assert data[:8] == b'GRLENS\x01\x00' + assert struct.unpack_from(' #include +/* Self-contained legacy v2 lens-map fixture writer. The production writer now + * emits v3, so this serializes a real little-endian v2 map (v2 provenance, no + * per-vertex cost counters) from a live mesh to keep the import path covered by + * genuine bytes instead of a hand-maintained golden blob. */ +static uint32_t v2_crc32(uint32_t crc, const void *data, size_t size) { + const unsigned char *bytes = data; + for (size_t i = 0; i < size; ++i) { + crc ^= bytes[i]; + for (int bit = 0; bit < 8; ++bit) + crc = (crc >> 1) ^ (0xedb88320u & (uint32_t)-(int)(crc & 1)); + } + return crc; +} +static int v2_fwrite_u32(FILE *f, uint32_t v) { + unsigned char b[4] = {(unsigned char)v, (unsigned char)(v >> 8), + (unsigned char)(v >> 16), (unsigned char)(v >> 24)}; + return fwrite(b, 1, sizeof b, f) == sizeof b ? 0 : -1; +} +static int v2_fwrite_u64(FILE *f, uint64_t v) { + unsigned char b[8]; + for (int i = 0; i < 8; ++i) b[i] = (unsigned char)(v >> (8 * i)); + return fwrite(b, 1, sizeof b, f) == sizeof b ? 0 : -1; +} +static int v2_fwrite_double(FILE *f, double v) { + uint64_t bits; memcpy(&bits, &v, sizeof bits); + return v2_fwrite_u64(f, bits); +} +static int write_v2_lens_map(const char *path, int width, int height, double fov, + const LensMapProvenance *p, + const LensMapFrame *frame) { + static const unsigned char magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0}; + FILE *f = fopen(path, "wb"); + if (f == NULL) return -1; + int failed = fwrite(magic, 1, sizeof magic, f) != sizeof magic || + v2_fwrite_u32(f, 2) || v2_fwrite_u32(f, 0x01020304u) || + v2_fwrite_u32(f, (uint32_t)width) || v2_fwrite_u32(f, (uint32_t)height) || + v2_fwrite_double(f, fov) || v2_fwrite_u64(f, 1) || + v2_fwrite_u32(f, p->threshold_kind) || + v2_fwrite_u32(f, p->threshold_policy_version) || + v2_fwrite_double(f, p->threshold_value) || + v2_fwrite_u32(f, p->retry_step_increment) || + v2_fwrite_u32(f, p->max_total_steps) || v2_fwrite_u32(f, p->max_level) || + v2_fwrite_u32(f, p->integrator) || + v2_fwrite_double(f, p->min_edge_pixels) || + v2_fwrite_double(f, p->min_area_pixels2) || + v2_fwrite_double(f, p->coordinate_time_step) || + v2_fwrite_u32(f, p->initial_max_steps); + const FrameLensMesh *m = &frame->mesh; + failed = failed || v2_fwrite_u64(f, frame->frame_id) || + v2_fwrite_double(f, frame->coordinate_time) || + v2_fwrite_double(f, frame->proper_time) || + v2_fwrite_u64(f, (uint64_t)m->vertex_count) || + v2_fwrite_u64(f, (uint64_t)m->triangle_count) || + v2_fwrite_u64(f, (uint64_t)m->retry_requests); + /* The payload CRC covers vertices+triangles only; the header stays + * deliberately independent, exactly as in the production writers. */ + uint32_t crc = UINT32_MAX; + for (size_t i = 0; i < m->vertex_count; ++i) { + const LensVertex *v = &m->vertices[i]; + const double vals[9] = {v->image_x, v->image_y, v->camera_direction[0], + v->camera_direction[1], v->camera_direction[2], + v->n_infinity[0], v->n_infinity[1], v->n_infinity[2], + v->log_frequency_ratio}; + const uint32_t tail[3] = {(uint32_t)v->end_id, (uint32_t)v->outcome, + (uint32_t)v->reason}; + unsigned char b[84]; size_t off = 0; + for (int k = 0; k < 9; ++k) { + uint64_t bits; memcpy(&bits, &vals[k], sizeof bits); + for (int q = 0; q < 8; ++q) b[off++] = (unsigned char)(bits >> (8 * q)); + } + for (int k = 0; k < 3; ++k) { + b[off++] = (unsigned char)tail[k]; + b[off++] = (unsigned char)(tail[k] >> 8); + b[off++] = (unsigned char)(tail[k] >> 16); + b[off++] = (unsigned char)(tail[k] >> 24); + } + if (fwrite(b, 1, sizeof b, f) != sizeof b) failed = 1; + crc = v2_crc32(crc, b, sizeof b); + } + for (size_t i = 0; i < m->triangle_count; ++i) { + unsigned char b[32]; size_t off = 0; + for (int j = 0; j < 3; ++j) { + const uint64_t idx = (uint64_t)m->triangles[i].vertex[j]; + for (int q = 0; q < 8; ++q) b[off++] = (unsigned char)(idx >> (8 * q)); + } + const uint32_t tail[2] = {m->triangles[i].level, + (uint32_t)m->triangles[i].approx_black}; + for (int k = 0; k < 2; ++k) { + b[off++] = (unsigned char)tail[k]; + b[off++] = (unsigned char)(tail[k] >> 8); + b[off++] = (unsigned char)(tail[k] >> 16); + b[off++] = (unsigned char)(tail[k] >> 24); + } + if (fwrite(b, 1, sizeof b, f) != sizeof b) failed = 1; + crc = v2_crc32(crc, b, sizeof b); + } + if (v2_fwrite_u32(f, crc ^ UINT32_MAX)) failed = 1; + if (fclose(f)) failed = 1; + return failed ? -1 : 0; +} + static int mesh_has_hanging_vertex(const FrameLensMesh *mesh) { for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle) for (size_t side = 0; side < 3; ++side) { @@ -139,6 +240,9 @@ static int review_probe_regressions(void) { m.vertices[i].n_infinity[0]=1; } m.vertices[0].outcome=RAY_OUTCOME_UNRESOLVED; + /* A genuinely budget-exhausted vertex has consumed its accepted-step + * quota; the retry layer keys on that blocking quota. */ + m.vertices[0].continuation_steps=10; m.vertices[0].continuation_limit=10; if (frame_lens_mesh_prepare_generation(&m,&c)<1 || m.retry_requests==0) return -1; /* the retry counter is produced by real retry requests */ @@ -153,11 +257,408 @@ static int review_probe_regressions(void) { return 0; } +/* A budget-exhausted vertex carrying the full adaptive resume state, so the + * retry layer sees the same payload the production store_endpoint path writes. */ +static LensVertex unresolved_vertex(double image_x, double t, double start, + unsigned int steps, + unsigned int step_limit, + double lookback_limit) { + LensVertex v; + memset(&v, 0, sizeof v); + v.image_x = image_x; + v.image_y = 0.0; + v.camera_direction[0] = 1.0; + v.outcome = RAY_OUTCOME_UNRESOLVED; + v.traced = 1; + v.continuation_t = t; + v.continuation_x[0] = 1.0; + v.continuation_Pi[0] = -1.0; + v.continuation_log_alpha_p0 = 0.125; + v.continuation_log_alpha_p0_0 = 0.5; + v.continuation_steps = steps; + v.continuation_limit = step_limit; + v.continuation_integration_start_time = start; + v.continuation_next_step = 0.25; + v.continuation_rejected_steps = 2; + v.continuation_rhs_evaluations = 20; + v.continuation_previous_rejected = 0; + v.continuation_lookback_limit = lookback_limit; + return v; +} + +static int uuu_fixture(LensVertex vertices[3], LensTriangle *triangle) { + for (int i = 0; i < 3; ++i) + vertices[i].camera_direction[0] = 1.0; + *triangle = (LensTriangle){{0, 1, 2}, 0, 0, 0}; + return 0; +} + +/* Independent step/time retry budgets: a request is allowed only while every + * quota that actually blocked the ray can still grow, and the two quotas are + * saturated separately. Also verifies the resume-state round trip used by + * both the frame and movie paths. */ +static int review_retry_quota_regressions(void) { + /* 1. Step-blocked, both quotas growable: the step quota advances, and the + * request records the independently saturated time quota. */ + { + LensVertex vertices[3] = { + unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0), + unresolved_vertex(10.0, -1.0, 0.0, 20, 20, 2.0), + unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + RefinementConfig c = {.max_level = 0, + .retry_step_increment = 10, + .max_total_steps = 100, + .retry_lookback_increment = 1.0, + .max_total_lookback_time = 10.0}; + if (frame_lens_mesh_prepare_generation(&m, &c) != 3) { + free(m.samples); free(m.probe_slots); + return -1; + } + for (size_t i = 0; i < m.sample_count; ++i) + if (m.samples[i].kind != FRAME_SAMPLE_RETRY || + m.samples[i].step_limit != 30 || + m.samples[i].lookback_limit != 3.0) { + free(m.samples); free(m.probe_slots); + return -1; + } + free(m.samples); free(m.probe_slots); + } + /* 2. Step quota at its cap while time can still grow: no request, because + * more time cannot buy an accepted step. The UUU triangle stays a + * budget-incomplete boundary, never blackened. */ + { + LensVertex vertices[3] = { + unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0), + unresolved_vertex(10.0, -1.0, 0.0, 20, 20, 2.0), + unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + RefinementConfig c = {.max_level = 0, + .retry_step_increment = 10, + .max_total_steps = 20, + .retry_lookback_increment = 1.0, + .max_total_lookback_time = 10.0}; + if (frame_lens_mesh_prepare_generation(&m, &c) != 0) { + free(m.samples); free(m.probe_slots); + return -1; + } + FrameBoundaryStats stats; + frame_lens_mesh_boundary_stats(&m, &c, &stats); + if (stats.uuu != 1 || stats.budget_incomplete_triangles == 0 || + stats.approx_black_triangles != 0) { + free(m.samples); free(m.probe_slots); + return -1; + } + free(m.samples); free(m.probe_slots); + } + /* 3. Time-blocked with a disabled step increment: the time quota alone + * enables the retry and the step budget is left untouched. */ + { + LensVertex vertices[3] = { + unresolved_vertex(0.0, -3.0, 0.0, 5, 10, 2.0), + unresolved_vertex(10.0, -3.0, 0.0, 5, 10, 2.0), + unresolved_vertex(0.0, -3.0, 0.0, 5, 10, 2.0)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + RefinementConfig c = {.max_level = 0, + .retry_step_increment = 0, + .max_total_steps = 10, + .retry_lookback_increment = 1.0, + .max_total_lookback_time = 10.0}; + if (frame_lens_mesh_prepare_generation(&m, &c) != 3) { + free(m.samples); free(m.probe_slots); + return -1; + } + for (size_t i = 0; i < m.sample_count; ++i) + if (m.samples[i].kind != FRAME_SAMPLE_RETRY || + m.samples[i].step_limit != 10 || + m.samples[i].lookback_limit != 3.0) { + free(m.samples); free(m.probe_slots); + return -1; + } + free(m.samples); free(m.probe_slots); + } + /* 4. Both blocking quotas at their caps: no request. */ + { + LensVertex vertices[3] = { + unresolved_vertex(0.0, -6.0, 0.0, 20, 20, 5.0), + unresolved_vertex(10.0, -6.0, 0.0, 20, 20, 5.0), + unresolved_vertex(0.0, -6.0, 0.0, 20, 20, 5.0)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + RefinementConfig c = {.max_level = 0, + .retry_step_increment = 10, + .max_total_steps = 20, + .retry_lookback_increment = 1.0, + .max_total_lookback_time = 5.0}; + if (frame_lens_mesh_prepare_generation(&m, &c) != 0) { + free(m.samples); free(m.probe_slots); + return -1; + } + free(m.samples); free(m.probe_slots); + } + /* 5. Resume-state round trip: the helper reproduces every control field, and + * install back-fills the vertex with the endpoint's new state and quota. */ + { + LensVertex source = unresolved_vertex(0.0, -1.0, 0.0, 20, 20, 2.0); + GeodesicRayState state; + if (frame_vertex_continuation_state(&source, &state) || + state.coordinate_time != -1.0 || state.steps != 20 || + state.integration_start_time != 0.0 || state.next_step != 0.25 || + state.rejected_steps != 2 || state.rhs_evaluations != 20 || + state.previous_rejected != 0 || state.log_alpha_p0 != 0.125 || + state.log_alpha_p0_0 != 0.5 || state.x[0] != 1.0 || + state.Pi[0] != -1.0) + return -1; + LensVertex target = {.image_x = 0.0, .camera_direction = {1.0, 0.0, 0.0}}; + FrameLensMesh mesh = {.vertices = &target, .vertex_count = 1}; + RefinementConfig plain = {.max_level = 0}; + if (frame_lens_mesh_prepare_generation(&mesh, &plain) != 1) { + free(mesh.samples); free(mesh.probe_slots); + return -1; + } + const RayEndpoint endpoint = { + .magnification = 1.0, + .end_id = SPACETIME_END_NONE, + .outcome = RAY_OUTCOME_UNRESOLVED, + .reason = RAY_REASON_BUDGET_EXHAUSTED, + .stop_coordinate_time = -4.0, + .accepted_steps = 7, + .final_x = {5.0, 0.0, 0.0}, + .final_Pi = {-1.0, 0.0, 0.0}, + .final_log_alpha_p0 = 0.125, + .final_log_alpha_p0_0 = 0.5, + .threshold_value = NAN, + .integration_start_time = 0.0, + .next_step = 0.5, + .rejected_steps = 3, + .rhs_evaluations = 70, + .previous_rejected = 1, + .lookback_limit = 2.5}; + if (frame_lens_mesh_install_sample(&mesh, 0, &endpoint) || + target.outcome != RAY_OUTCOME_UNRESOLVED || + target.continuation_t != -4.0 || target.continuation_steps != 7 || + target.continuation_limit != 7 || + target.continuation_integration_start_time != 0.0 || + target.continuation_next_step != 0.5 || + target.continuation_rejected_steps != 3 || + target.continuation_rhs_evaluations != 70 || + target.continuation_previous_rejected != 1 || + target.continuation_lookback_limit != 2.5) { + free(mesh.samples); free(mesh.probe_slots); + return -1; + } + GeodesicRayState rebuilt; + if (frame_vertex_continuation_state(&target, &rebuilt) || + rebuilt.coordinate_time != -4.0 || rebuilt.steps != 7 || + rebuilt.next_step != 0.5 || rebuilt.rejected_steps != 3 || + rebuilt.rhs_evaluations != 70 || rebuilt.previous_rejected != 1) { + free(mesh.samples); free(mesh.probe_slots); + return -1; + } + free(mesh.samples); free(mesh.probe_slots); + } + return 0; +} + +static GeodesicTraceConfig frame_dp_config(double lookback) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.stepper = GEODESIC_STEPPER_DP54; + c.coordinate_time_step = 0.5; + c.max_steps = 100; + c.threshold = (ThresholdPolicy){.kind = THRESHOLD_DISABLED, .value = 0.0, + .policy_version = 0}; + c.atol_x = c.atol_Pi = c.atol_L = c.rtol = 1e-9; + c.min_step = 1e-12; + c.max_step = 1e6; + c.consecutive_rejection_limit = 1000; + c.max_lookback_time = lookback; + return c; +} + +/* A real production trace must record the configured step/time grant distinct + * from the spent counts, so a time-limited ray can retry with extra time while + * keeping its full step grant. A retry candidate also counts only when it + * opens a strictly larger representable region. */ +static int review_real_grant_and_time_growth(void) { + SpacetimeSource source = {0}; + if (spacetime_create_minkowski(&source, 10.0)) + return -1; + const ObserverState observer = observer_fixed_at_origin(); + int failed = 0; + + /* A. Real trace at the origin, time-limited after ~2 of 100 granted steps. + * With the step increment disabled and the time increment enabled, the + * retry must keep step_limit == 100 and only grow the time budget. */ + { + LensVertex vertices[3] = {{.camera_direction = {1.0, 0.0, 0.0}}, + {.camera_direction = {1.0, 0.0, 0.0}}, + {.camera_direction = {1.0, 0.0, 0.0}}}; + LensTriangle triangle = {{0, 1, 2}, 0, 0, 0}; + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + const GeodesicTraceConfig trace = frame_dp_config(1.0); + if (frame_lens_mesh_trace(&m, &source, &observer, &trace)) + failed = 1; + for (size_t i = 0; i < 3 && !failed; ++i) { + if (vertices[i].outcome != RAY_OUTCOME_UNRESOLVED || + vertices[i].continuation_limit != 100u || + vertices[i].continuation_steps >= 100u || + vertices[i].continuation_lookback_limit != 1.0) + failed = 1; + } + const RefinementConfig retry = {.max_level = 0, + .retry_step_increment = 0, + .max_total_steps = 100, + .retry_lookback_increment = 1.0, + .max_total_lookback_time = 100.0}; + if (!failed && frame_lens_mesh_prepare_generation(&m, &retry) != 3) + failed = 1; + for (size_t i = 0; i < m.sample_count && !failed; ++i) + if (m.samples[i].kind != FRAME_SAMPLE_RETRY || + m.samples[i].step_limit != 100u || + m.samples[i].lookback_limit != 2.0) + failed = 1; + free(m.samples); + free(m.probe_slots); + } + + /* B. Translated time origins (positive, negative and zero) still detect the + * lookback boundary with the integrator's own comparison and retry with a + * strictly larger time region while preserving the step grant. */ + for (int origin = 0; origin < 3 && !failed; ++origin) { + const double t0 = origin == 0 ? 0.0 : origin == 1 ? 1.0e9 : -1.0e9; + ObserverState o = observer_fixed_at_origin(); + o.coordinate_time = t0; + const GeodesicTraceConfig trace = frame_dp_config(0.3); + MetricData metric; + if (spacetime_eval(&source, t0, o.coordinate_position, &metric)) { + failed = 1; + break; + } + GeodesicRayState state; + if (geodesic_initialize_past_ray_metric(&metric, &o, (double[]){1, 0, 0}, + &state)) { + failed = 1; + break; + } + const RayEndpoint endpoint = + geodesic_trace_past_from_state(&source, &state, &trace); + if (endpoint.outcome != RAY_OUTCOME_UNRESOLVED || + endpoint.accepted_step_limit != 100u || endpoint.accepted_steps != 1u) { + failed = 1; + break; + } + LensVertex vertices[3]; + memset(vertices, 0, sizeof vertices); + vertices[1].traced = vertices[2].traced = 1; + vertices[1].outcome = vertices[2].outcome = RAY_OUTCOME_ESCAPED; + vertices[1].n_infinity[0] = vertices[2].n_infinity[0] = 1.0; + vertices[1].end_id = vertices[2].end_id = 0; + LensTriangle triangle = {{0, 1, 2}, 0, 0, 0}; + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + const RefinementConfig plain = {.max_level = 0}; + if (frame_lens_mesh_prepare_generation(&m, &plain) != 1 || + frame_lens_mesh_install_sample(&m, 0, &endpoint) || + vertices[0].continuation_limit != 100u || + vertices[0].continuation_lookback_limit != 0.3) { + free(m.samples); + free(m.probe_slots); + failed = 1; + break; + } + free(m.samples); + m.samples = NULL; + m.sample_count = m.sample_capacity = 0; + const RefinementConfig retry = {.max_level = 0, + .retry_step_increment = 0, + .max_total_steps = 100, + .retry_lookback_increment = 0.3, + .max_total_lookback_time = 10.0}; + if (frame_lens_mesh_prepare_generation(&m, &retry) != 1 || + m.samples[0].kind != FRAME_SAMPLE_RETRY || + m.samples[0].step_limit != 100u || + m.samples[0].lookback_limit != 0.6) + failed = 1; + free(m.samples); + free(m.probe_slots); + } + + /* C. A larger quota that does not move the left boundary (increment rounds + * away, or a large time origin absorbs it) must not launch a retry. */ + { + LensVertex vertices[3] = { + unresolved_vertex(0.0, -1.0e9, 0.0, 1, 100, 1.0e9), + unresolved_vertex(10.0, -1.0e9, 0.0, 1, 100, 1.0e9), + unresolved_vertex(0.0, -1.0e9, 0.0, 1, 100, 1.0e9)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + const RefinementConfig rounded = {.max_level = 0, + .retry_step_increment = 0, + .max_total_steps = 100, + .retry_lookback_increment = 1.0e-8, + .max_total_lookback_time = 2.0e9}; + if (frame_lens_mesh_prepare_generation(&m, &rounded) != 0) + failed = 1; + FrameBoundaryStats stats; + frame_lens_mesh_boundary_stats(&m, &rounded, &stats); + if (stats.uuu != 1 || stats.budget_incomplete_triangles == 0) + failed = 1; + free(m.samples); + free(m.probe_slots); + } + { + const double start = 1.0e12; + const double quota = 1.0e6; + LensVertex vertices[3] = { + unresolved_vertex(0.0, start - quota, start, 1, 100, quota), + unresolved_vertex(10.0, start - quota, start, 1, 100, quota), + unresolved_vertex(0.0, start - quota, start, 1, 100, quota)}; + LensTriangle triangle; + uuu_fixture(vertices, &triangle); + FrameLensMesh m = {.vertices = vertices, .vertex_count = 3, + .triangles = &triangle, .triangle_count = 1}; + const RefinementConfig absorbed = {.max_level = 0, + .retry_step_increment = 0, + .max_total_steps = 100, + .retry_lookback_increment = 1.0e-6, + .max_total_lookback_time = 2.0e6}; + if (frame_lens_mesh_prepare_generation(&m, &absorbed) != 0) + failed = 1; + free(m.samples); + free(m.probe_slots); + } + spacetime_destroy(&source); + return failed ? -1 : 0; +} + int main(void) { if (review_probe_regressions()) { fputs("probe persistence / retry invalidation regression failed\n",stderr); return 1; } + if (review_retry_quota_regressions()) { + fputs("independent retry quota regression failed\n", stderr); + return 1; + } + if (review_real_grant_and_time_growth()) { + fputs("real grant / representable time growth regression failed\n", stderr); + return 1; + } const int width = 100, height = 100; const double test_exposure = 1e-3; const double psf_relative_tail = 1e-8; @@ -187,7 +688,7 @@ int main(void) { } /* A finalized mesh can be persisted independently of spacetime and then * drive the exact same catalog inverse-map and PSF pass. */ - const char *lens_map_path = "/tmp/gr_lens_map_test.grlens"; + const char *lens_map_path = "/tmp/opencode/gr_lens_map_test.grlens"; mesh.retry_requests = 2; /* cumulative per-frame retry accounting round-trips */ const LensMapFrame saved_frame = {.frame_id = 7, .coordinate_time = 3.0, @@ -259,7 +760,7 @@ int main(void) { /* A version-1 header must be rejected outright: its captured bit cannot be * upgraded into the new dark/unresolved/error provenance. */ { - const char *legacy_path = "/tmp/gr_lens_map_v1_test.grlens"; + const char *legacy_path = "/tmp/opencode/gr_lens_map_v1_test.grlens"; FILE *legacy = fopen(legacy_path, "wb"); int legacy_failed = legacy == NULL; if (!legacy_failed) { @@ -906,6 +1407,147 @@ int main(void) { } free(ud_mesh.probe_slots); } + /* v3 DP54 round-trip: every adaptive/quota field and the per-vertex cost + * counters must survive the wire exactly. */ + { + const char *dp_path = "/tmp/opencode/gr_lens_map_v3_dp_test.grlens"; + LensVertex dv[3]; + for (int i = 0; i < 3; ++i) + dv[i] = (LensVertex){.image_x = (double)i, .image_y = 2.0, + .camera_direction = {0.0, 0.0, -1.0}, + .outcome = RAY_OUTCOME_DARK, + .reason = RAY_REASON_REDSHIFT_LIMIT, + .end_id = SPACETIME_END_NONE, .traced = 1}; + dv[0].trace_accepted_steps = 11; dv[0].trace_rejected_steps = 2; + dv[0].trace_rhs_evaluations = 79; + dv[1].trace_accepted_steps = 5; + LensTriangle dt = {{0, 1, 2}, 1, 1, 0}; + FrameLensMesh dm = {.vertices = dv, .triangles = &dt, .vertex_count = 3, + .vertex_capacity = 3, .triangle_count = 1, + .triangle_capacity = 1}; + LensMapFrame df = {.frame_id = 3, .coordinate_time = 1.5, + .proper_time = 1.25, .mesh = dm}; + const LensMapProvenance dp = {.threshold_kind = THRESHOLD_LOG_ENERGY_GROWTH, + .threshold_policy_version = 3, .threshold_value = 8.0, + .retry_step_increment = 64, .max_total_steps = 256, .max_level = 2, + .integrator = (uint32_t)GEODESIC_STEPPER_DP54, + .min_edge_pixels = 0.5, .min_area_pixels2 = 0.25, + .coordinate_time_step = 0.1, .initial_max_steps = 1024, + .atol_x = 1e-9, .atol_Pi = 1e-9, .atol_L = 1e-9, .rtol = 1e-9, + .min_step = 1e-12, .max_step = 2.0, .max_lookback_time = 102.4, + .retry_lookback_increment = 102.4, .max_total_lookback_time = 409.6, + .max_consecutive_rejections = 32}; + LensMap dloaded = {0}; + if (lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1) || + lens_map_read(dp_path, NULL, &dloaded)) { + fputs("lens-map v3 DP54 round-trip regression failed\n", stderr); + lens_map_destroy(&dloaded); unlink(dp_path); goto done; + } + const LensMapProvenance *lp = &dloaded.provenance; + if (dloaded.file_version != 3 || + lp->integrator != (uint32_t)GEODESIC_STEPPER_DP54 || + lp->atol_x != 1e-9 || lp->atol_Pi != 1e-9 || lp->atol_L != 1e-9 || + lp->rtol != 1e-9 || lp->min_step != 1e-12 || lp->max_step != 2.0 || + lp->max_lookback_time != 102.4 || + lp->retry_lookback_increment != 102.4 || + lp->max_total_lookback_time != 409.6 || + lp->max_consecutive_rejections != 32 || + lp->retry_step_increment != 64 || lp->max_total_steps != 256 || + lp->coordinate_time_step != 0.1 || lp->initial_max_steps != 1024 || + dloaded.frames[0].mesh.vertices[0].trace_accepted_steps != 11 || + dloaded.frames[0].mesh.vertices[0].trace_rejected_steps != 2 || + dloaded.frames[0].mesh.vertices[0].trace_rhs_evaluations != 79 || + dloaded.frames[0].mesh.vertices[1].trace_accepted_steps != 5 || + dloaded.frames[0].mesh.triangles[0].level != 1) { + fputs("lens-map v3 DP54 field round-trip regression failed\n", stderr); + lens_map_destroy(&dloaded); unlink(dp_path); goto done; + } + lens_map_destroy(&dloaded); + /* Unknown wire code and non-finite/out-of-bounds DP fields must be rejected + * by the shared schema validator, not accepted as a usable map. */ + { + /* offset, is_double, double_value, u32_value */ + const struct { long offset; int is_double; double dvalue; uint32_t uvalue; } + corruptions[4] = {{68, 0, 0.0, 7u}, /* unknown integrator code */ + {100, 1, NAN, 0u}, /* atol_x = NaN */ + {108, 1, -1.0, 0u}, /* atol_Pi below zero */ + {156, 1, -1.0, 0u}}; /* max_lookback below zero */ + for (size_t c = 0; c < 4; ++c) { + if (lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1)) { + fputs("lens-map v3 corruption fixture write failed\n", stderr); + unlink(dp_path); goto done; + } + FILE *bad = fopen(dp_path, "r+b"); + int bad_failed = + bad == NULL || fseek(bad, corruptions[c].offset, SEEK_SET); + if (!bad_failed) { + if (corruptions[c].is_double) + bad_failed = fwrite(&corruptions[c].dvalue, + sizeof(double), 1, bad) != 1; + else + bad_failed = fwrite(&corruptions[c].uvalue, + sizeof(uint32_t), 1, bad) != 1; + } + if (bad != NULL && fclose(bad)) bad_failed = 1; + if (bad_failed || !lens_map_read(dp_path, NULL, &dloaded)) { + fputs("lens-map v3 invalid DP54 field rejection regression failed\n", + stderr); + lens_map_destroy(&dloaded); unlink(dp_path); goto done; + } + lens_map_destroy(&dloaded); + } + } + unlink(dp_path); + } + /* Legacy v2 import: a real v2 map (no adaptive fields, no cost counters) + * must load as RK4 with an explicit zero adaptive policy and render + * identically to the live mesh. */ + { + const char *v2_path = "/tmp/opencode/gr_lens_map_v2_legacy_test.grlens"; + const LensMapProvenance v2p = {.threshold_kind = THRESHOLD_LOG_ALPHA_P0, + .threshold_policy_version = 1, .threshold_value = 8.0, + .retry_step_increment = 16, .max_total_steps = 64, .max_level = 2, + .integrator = (uint32_t)GEODESIC_STEPPER_RK4, + .min_edge_pixels = 0.5, .min_area_pixels2 = 0.25, + .coordinate_time_step = 0.1, .initial_max_steps = 4096}; + const LensMapFrame v2f = {.frame_id = 7, .coordinate_time = 3.0, + .proper_time = 2.0, .mesh = mesh}; + LensMap v2loaded = {0}; + double *v2_hdr = calloc((size_t)width * height * 3, sizeof *v2_hdr); + double *live_hdr = calloc((size_t)width * height * 3, sizeof *live_hdr); + if (v2_hdr == NULL || live_hdr == NULL || + write_v2_lens_map(v2_path, width, height, 30.0, &v2p, &v2f) || + lens_map_read(v2_path, NULL, &v2loaded) || + v2loaded.file_version != 2 || + v2loaded.provenance.integrator != (uint32_t)GEODESIC_STEPPER_RK4 || + v2loaded.provenance.atol_x != 0.0 || + v2loaded.provenance.max_lookback_time != 0.0 || + v2loaded.provenance.max_total_lookback_time != 0.0 || + v2loaded.provenance.max_consecutive_rejections != 0 || + v2loaded.frames[0].mesh.vertices[0].trace_accepted_steps != 0 || + v2loaded.frames[0].mesh.vertices[0].trace_rhs_evaluations != 0) { + fputs("lens-map v2 legacy import regression failed\n", stderr); + free(v2_hdr); free(live_hdr); lens_map_destroy(&v2loaded); + unlink(v2_path); goto done; + } + const size_t live_images = frame_splat_catalog( + &mesh, &catalog, live_hdr, width, height, test_exposure, &psf, NULL, + INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, + FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL, NULL); + const size_t v2_images = frame_splat_catalog( + &v2loaded.frames[0].mesh, &catalog, v2_hdr, width, height, + test_exposure, &psf, NULL, INFINITY, 1.0, psf_relative_tail, 0.0, 0, 1, + FRAME_CATALOG_PREFETCH_FRAME, NULL, NULL, NULL, NULL, NULL); + int render_equal = live_images == v2_images && live_images == images; + for (int k = 0; render_equal && k < width * height * 3; ++k) + if (live_hdr[k] != v2_hdr[k]) render_equal = 0; + free(v2_hdr); free(live_hdr); + lens_map_destroy(&v2loaded); unlink(v2_path); + if (!render_equal) { + fputs("lens-map v2 legacy render mismatch regression failed\n", stderr); + goto done; + } + } result = 0; done: frame_lens_mesh_destroy(&mesh); diff --git a/tests/test_geodesic_adaptive.c b/tests/test_geodesic_adaptive.c new file mode 100644 index 0000000..fcd9f50 --- /dev/null +++ b/tests/test_geodesic_adaptive.c @@ -0,0 +1,2621 @@ +/* + * Focused regression for the explicit DP5(4) adaptive geodesic core and its + * RayPool integration. + * + * The production stepper (src/geodesic.c), the production RHS, the analytic + * Minkowski and Schwarzschild backends, the observer builder and the + * production RayPool scheduler are exercised directly. The single-trace + * geodesic core, `geodesic_advance_past_ray`, and the batch pool's activation, + * OMP bulk advance and resume-state handling are covered here. + * + * Both analytic providers define spacetime_create_default, so they are + * textually included with a renamed provider symbol; COMMON_SOURCES excludes + * them from the link line. + */ +#define spacetime_create_default spacetime_create_default_minkowski_adaptive +#include "../src/spacetime_minkowski.c" +#undef spacetime_create_default +#define spacetime_create_default spacetime_create_default_schwarzschild_adaptive +#include "../src/spacetime_schwarzschild.c" +#undef spacetime_create_default + +#include "asymptotic.h" +#include "asymptotic_schwarzschild.h" +#include "geodesic.h" +#include "observer.h" +#include "ray.h" +#include "spacetime.h" + +#include +#include +#include +#include +#include + +static int failures = 0; +#define CHECK(condition, message) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \ + ++failures; \ + } \ + } while (0) + +static double dot3(const double a[3], const double b[3]) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +static int invert3(const double g[3][3], double inv[3][3]) { + const double d = g[0][0] * (g[1][1] * g[2][2] - g[1][2] * g[2][1]) - + g[0][1] * (g[1][0] * g[2][2] - g[1][2] * g[2][0]) + + g[0][2] * (g[1][0] * g[2][1] - g[1][1] * g[2][0]); + if (!isfinite(d) || fabs(d) < 1e-300) + return -1; + inv[0][0] = (g[1][1] * g[2][2] - g[1][2] * g[2][1]) / d; + inv[0][1] = (g[0][2] * g[2][1] - g[0][1] * g[2][2]) / d; + inv[0][2] = (g[0][1] * g[1][2] - g[0][2] * g[1][1]) / d; + inv[1][0] = (g[1][2] * g[2][0] - g[1][0] * g[2][2]) / d; + inv[1][1] = (g[0][0] * g[2][2] - g[0][2] * g[2][0]) / d; + inv[1][2] = (g[0][2] * g[1][0] - g[0][0] * g[1][2]) / d; + inv[2][0] = (g[1][0] * g[2][1] - g[1][1] * g[2][0]) / d; + inv[2][1] = (g[0][1] * g[2][0] - g[0][0] * g[2][1]) / d; + inv[2][2] = (g[0][0] * g[1][1] - g[0][1] * g[1][0]) / d; + return 0; +} + +static double null_residual(const MetricData *m, const double Pi[3]) { + double inv[3][3]; + if (invert3(m->gamma, inv)) + return NAN; + double value = 0.0; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + value += inv[i][j] * Pi[i] * Pi[j]; + return value; +} + +/* Stationary KS Killing energy reference (oracle cross-check only). */ +static double killing_energy(const MetricData *m, const double Pi[3], + double log_alpha_p0) { + return exp(log_alpha_p0) * (m->alpha - dot3(m->beta, Pi)); +} + +static RayEndpoint blank_endpoint(void) { + RayEndpoint out; + memset(&out, 0, sizeof out); + out.magnification = 1.0; + out.end_id = SPACETIME_END_NONE; + out.outcome = RAY_OUTCOME_INCOMPLETE; + out.reason = RAY_REASON_NONE; + out.threshold_value = NAN; + out.stop_coordinate_time = NAN; + return out; +} + +static GeodesicTraceConfig dp_config(double tol, double initial_step, + double lookback) { + GeodesicTraceConfig c; + memset(&c, 0, sizeof c); + c.stepper = GEODESIC_STEPPER_DP54; + c.coordinate_time_step = initial_step; + c.max_steps = 1000000u; + c.threshold = (ThresholdPolicy){.kind = THRESHOLD_DISABLED, + .value = 0.0, + .policy_version = 0}; + c.atol_x = tol; + c.atol_Pi = tol; + c.atol_L = tol; + c.rtol = tol; + c.min_step = 1e-12; + c.max_step = 1e6; + c.consecutive_rejection_limit = 1000u; + c.max_lookback_time = lookback; + return c; +} + +static ObserverState flat_observer_at(const double p[3]) { + ObserverState o; + memset(&o, 0, sizeof o); + o.coordinate_position[0] = p[0]; + o.coordinate_position[1] = p[1]; + o.coordinate_position[2] = p[2]; + o.tetrad[0][0] = 1.0; + o.tetrad[1][1] = 1.0; + o.tetrad[2][2] = 1.0; + o.tetrad[3][3] = 1.0; + return o; +} + +static MetricData flat_metric(void) { + MetricData m; + memset(&m, 0, sizeof m); + m.alpha = 1.0; + m.gamma[0][0] = m.gamma[1][1] = m.gamma[2][2] = 1.0; + return m; +} + +/* ------------------------------------------------------------------ */ +/* Synthetic fixture: flat / linear-potential curved metric with a */ +/* configurable spatial domain, invalid-metric and unavailable-time */ +/* injection windows. Declares a single fixed Minkowski end. */ +/* ------------------------------------------------------------------ */ + +typedef struct { + int has_domain; /* enable the [domain_lo, domain_hi] wall check */ + double domain_lo, domain_hi; + SpacetimePointStatus bad_status; /* injection status (OK disables) */ + double bad_lo, bad_hi; + double radius; /* escape worldtube radius */ +} FixtureContext; + +static SpacetimePointStatus fixture_eval(const SpacetimeSource *source, + double t, const double x[3], + MetricData *metric) { + const FixtureContext *c = source->context; + if (c->bad_status != SPACETIME_POINT_OK && t >= c->bad_lo && t <= c->bad_hi) + return c->bad_status; + if (c->has_domain && x[0] >= c->domain_lo && x[0] <= c->domain_hi) + return SPACETIME_POINT_OUT_OF_DOMAIN; + *metric = flat_metric(); + return SPACETIME_POINT_OK; +} + +static SpacetimeRayStatus fixture_classify(const SpacetimeSource *source, + double t, const double x[3]) { + (void)source; + (void)t; + (void)x; + return SPACETIME_RAY_ACTIVE; +} + +static size_t fixture_end_count(const SpacetimeSource *source) { + (void)source; + return 1; +} + +static int fixture_end(const SpacetimeSource *source, size_t index, + SpacetimeAsymptoticEnd *out) { + (void)source; + if (index != 0) + return -1; + *out = (SpacetimeAsymptoticEnd){ + .end_id = 0, + .exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI, + .mass = 0.0, + .frame_origin = {0.0, 0.0, 0.0}, + .frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + return 0; +} + +static int fixture_worldtube(const SpacetimeSource *source, + SpacetimeEndId end_id, double t, + SpacetimeEscapeWorldtubeSample *out) { + const FixtureContext *c = source->context; + (void)t; + if (end_id != 0) + return -1; + *out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0}, + .velocity = {0.0, 0.0, 0.0}, + .radius = c->radius, + .radius_rate = 0.0, + .velocity_constant = 1, + .valid = 1}; + return 0; +} + +static void fixture_destroy(SpacetimeSource *source) { + source->context = NULL; + source->ops = NULL; +} + +static const SpacetimeOps fixture_ops = { + .eval = fixture_eval, + .classify = fixture_classify, + .asymptotic_end_count = fixture_end_count, + .asymptotic_end = fixture_end, + .escape_worldtube_sample = fixture_worldtube, + .destroy = fixture_destroy, +}; + +/* ------------------------------------------------------------------ */ +/* 1. Minkowski finite interval: DP accuracy, translation, slabs, count */ +/* ------------------------------------------------------------------ */ + +static void test_minkowski_finite_interval(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_minkowski(&source, 1.0e9) == 0, "create minkowski"); + const double position[3] = {3.0, -4.0, 5.0}; + const double direction[3] = {0.36, 0.48, 0.8}; + const ObserverState observer = flat_observer_at(position); + MetricData metric; + CHECK(spacetime_eval(&source, 0.0, position, &metric) == SPACETIME_POINT_OK, + "minkowski metric"); + GeodesicRayState state; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &state) == 0, + "minkowski init"); + const GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6); + state.next_step = config.coordinate_time_step; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, "mink slab"); + RayEndpoint endpoint = blank_endpoint(); + const GeodesicAdvanceResult result = + geodesic_advance_past_ray(slab, &state, -2.0, &config, &endpoint); + CHECK(result == GEODESIC_ADVANCE_ACTIVE, "finite interval stays active"); + /* Flat RHS: x(t) = x0 + (t - t0) * Pi and L constant. */ + CHECK(fabs(state.coordinate_time + 2.0) < 1e-12, "reached interval end"); + for (int i = 0; i < 3; ++i) { + const double expected = position[i] - 2.0 * state.Pi[i]; + CHECK(fabs(state.x[i] - expected) < 1e-9, "minkowski translation exact"); + } + CHECK(fabs(state.log_alpha_p0) < 1e-12, "flat L stays zero"); + CHECK(state.rhs_evaluations == 7u * ((unsigned long)state.steps + + (unsigned long)state.rejected_steps), + "actual RHS count equals full DP trials"); + CHECK(state.steps > 0, "accepted at least one step"); + spacetime_free_slab(slab); + + /* Translation independence: the same boost shifted by a constant vector + * produces the same state shifted by that vector. */ + { + const double shift[3] = {100.0, -200.0, 300.0}; + double shifted[3]; + for (int i = 0; i < 3; ++i) + shifted[i] = position[i] + shift[i]; + const ObserverState observer2 = flat_observer_at(shifted); + MetricData metric2; + CHECK(spacetime_eval(&source, 0.0, shifted, &metric2) == SPACETIME_POINT_OK, + "shifted metric"); + GeodesicRayState state2; + CHECK(geodesic_initialize_past_ray_metric(&metric2, &observer2, direction, + &state2) == 0, + "shifted init"); + state2.next_step = config.coordinate_time_step; + MetricSlab *slab2 = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab2) == 0, + "shifted slab"); + RayEndpoint endpoint2 = blank_endpoint(); + (void)geodesic_advance_past_ray(slab2, &state2, -2.0, &config, &endpoint2); + for (int i = 0; i < 3; ++i) + CHECK(fabs((state2.x[i] - shift[i]) - state.x[i]) < 1e-9, + "translation invariance"); + spacetime_free_slab(slab2); + } + + /* Cross-slab continuity: two partial slabs must match one long slab. */ + { + GeodesicRayState split; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &split) == 0, + "split init"); + split.next_step = config.coordinate_time_step; + MetricSlab *first = NULL, *second = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.5, &first) == 0, "first slab"); + RayEndpoint e1 = blank_endpoint(); + const GeodesicAdvanceResult r1 = + geodesic_advance_past_ray(first, &split, -1.0, &config, &e1); + spacetime_free_slab(first); + CHECK(r1 == GEODESIC_ADVANCE_ACTIVE, "first partial slab active"); + CHECK(spacetime_load_slab(&source, split.coordinate_time, -3.0, &second) == + 0, + "second slab"); + RayEndpoint e2 = blank_endpoint(); + const GeodesicAdvanceResult r2 = + geodesic_advance_past_ray(second, &split, -2.0, &config, &e2); + spacetime_free_slab(second); + CHECK(r2 == GEODESIC_ADVANCE_ACTIVE, "second partial slab active"); + for (int i = 0; i < 3; ++i) + CHECK(fabs(split.x[i] - state.x[i]) < 1e-7, "cross-slab continuity x"); + CHECK(fabs(split.log_alpha_p0 - state.log_alpha_p0) < 1e-9, + "cross-slab continuity L"); + } + + /* A tiny slab-left interval must still accept a boundary-limited step even + * below min_step, without permanently depressing the next suggestion. */ + { + GeodesicRayState tiny; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &tiny) == 0, + "tiny init"); + GeodesicTraceConfig tiny_config = dp_config(1e-9, 1.0, 1.0e6); + tiny_config.min_step = 0.25; + tiny.next_step = tiny_config.coordinate_time_step; + MetricSlab *s = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -0.1, &s) == 0, "tiny slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(s, &tiny, -1e-6, &tiny_config, &out); + spacetime_free_slab(s); + CHECK(r == GEODESIC_ADVANCE_ACTIVE, "tiny boundary step active"); + CHECK(fabs(tiny.coordinate_time + 1e-6) < 1e-18, "tiny boundary reached"); + CHECK(tiny.next_step == tiny_config.coordinate_time_step, + "tiny boundary kept the original proposal"); + } + + /* Invalid DP configuration must be rejected explicitly, not defaulted. */ + { + GeodesicRayState bad_state; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &bad_state) == 0, + "bad config init"); + MetricSlab *s = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &s) == 0, "bad slab"); + GeodesicTraceConfig bad = dp_config(1e-9, 0.5, 1.0e6); + bad.atol_x = 0.0; + RayEndpoint out = blank_endpoint(); + CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == + GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "zero atol rejected"); + bad = dp_config(1e-9, 0.5, 1.0e6); + bad.max_lookback_time = 0.0; + CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == + GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "zero lookback rejected"); + bad = dp_config(1e-9, 0.5, 1.0e6); + bad.min_step = 2.0; + bad.max_step = 1.0; + CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == + GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "inverted step bounds rejected"); + bad = dp_config(1e-9, 0.5, 1.0e6); + bad.consecutive_rejection_limit = 0; + CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == + GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "zero reject limit rejected"); + spacetime_free_slab(s); + } + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 2. Schwarzschild radial branches, interior observer, residuals */ +/* ------------------------------------------------------------------ */ + +static void radial_static_observer(const MetricData *metric, double r0, + ObserverState *out) { + *out = (ObserverState){.coordinate_time = 0.0, + .coordinate_position = {r0, 0.0, 0.0}}; + const double alpha = metric->alpha; + out->tetrad[0][0] = 1.0 / alpha; + for (int i = 0; i < 3; ++i) + out->tetrad[0][i + 1] = -metric->beta[i] / alpha; + out->tetrad[1][1] = 1.0 / sqrt(metric->gamma[0][0]); + out->tetrad[2][2] = 1.0; + out->tetrad[3][3] = 1.0; +} + +static int trace_radial_dp(const SpacetimeSource *source, const ObserverState *o, + double direction, double duration, + const GeodesicTraceConfig *config, + GeodesicRayState *state) { + MetricData metric; + if (spacetime_eval(source, o->coordinate_time, o->coordinate_position, + &metric) != SPACETIME_POINT_OK) + return -1; + const double n[3] = {direction, 0.0, 0.0}; + if (geodesic_initialize_past_ray_metric(&metric, o, n, state)) + return -1; + state->next_step = config->coordinate_time_step; + MetricSlab *slab = NULL; + if (spacetime_load_slab(source, 0.0, -duration - 1.0, &slab)) + return -1; + RayEndpoint endpoint = blank_endpoint(); + const GeodesicAdvanceResult result = + geodesic_advance_past_ray(slab, state, -duration, config, &endpoint); + spacetime_free_slab(slab); + return result == GEODESIC_ADVANCE_FAILED ? -1 : 0; +} + +static void test_schwarzschild_radial(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0, + "create schwarzschild"); + const double r0 = 10.0; + MetricData metric; + CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) == + SPACETIME_POINT_OK, + "r0 metric"); + ObserverState observer; + radial_static_observer(&metric, r0, &observer); + const GeodesicTraceConfig config = dp_config(1e-7, 0.5, 1.0e6); + + GeodesicRayState outward; + CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &config, &outward) == 0, + "outward trace"); + const double s_out = -outward.coordinate_time; + CHECK(fabs(outward.x[0] - r0 - s_out) < 1e-4, + "outward closed-form invariant"); + MetricData out_metric; + CHECK(spacetime_eval(&source, outward.coordinate_time, outward.x, + &out_metric) == SPACETIME_POINT_OK, + "outward end metric"); + CHECK(fabs(null_residual(&out_metric, outward.Pi) - 1.0) < 1e-6, + "outward null constraint"); + + GeodesicRayState inward; + CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &config, &inward) == 0, + "inward trace"); + const double s_in = -inward.coordinate_time; + const double c0 = (r0 - 2.0) + 4.0 * log(r0 - 2.0); + const double c1 = (inward.x[0] - 2.0) + 4.0 * log(inward.x[0] - 2.0) + s_in; + CHECK(inward.x[0] > 2.0, "inward stays outside horizon"); + CHECK(inward.x[0] < r0, "inward decreases r"); + CHECK(fabs(c1 - c0) < 1e-4, "inward closed-form invariant"); + MetricData in_metric; + CHECK(spacetime_eval(&source, inward.coordinate_time, inward.x, &in_metric) == + SPACETIME_POINT_OK, + "inward end metric"); + CHECK(fabs(null_residual(&in_metric, inward.Pi) - 1.0) < 1e-6, + "inward null constraint"); + + /* Killing energy conservation along a radial ray. */ + { + GeodesicRayState start; + MetricData m0 = metric; + CHECK(geodesic_initialize_past_ray_metric( + &m0, &observer, (double[]){1.0, 0.0, 0.0}, &start) == 0, + "killing init"); + GeodesicRayState end; + CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &config, &end) == 0, + "killing trace"); + MetricData m1; + CHECK(spacetime_eval(&source, end.coordinate_time, end.x, &m1) == + SPACETIME_POINT_OK, + "killing end metric"); + const double ek_start = killing_energy(&m0, start.Pi, start.log_alpha_p0); + const double ek_end = killing_energy(&m1, end.Pi, end.log_alpha_p0); + CHECK(fabs(ek_end / ek_start - 1.0) < 1e-5, + "Killing energy conserved along DP ray"); + } + + /* Interior r = 1.5 free-fall-from-rest-at-infinity observer: legal + * timelike observer, null constraint along the integrated ray. */ + { + const double r = 1.5; + const double f = 1.0 - 2.0 / r; + const double ur = -sqrt(2.0 / r); + const double uks = 1.0 / f + (2.0 / (r - 2.0)) * ur; + const double vx = ur / uks; + ObserverCamera camera = {.position = {r, 0.0, 0.0}, + .velocity = {vx, 0.0, 0.0}, + .look_ra_deg = 0.0, + .look_dec_deg = 0.0, + .roll_deg = 0.0}; + MetricData m; + ObserverState inner; + CHECK(spacetime_eval(&source, 0.0, camera.position, &m) == + SPACETIME_POINT_OK && + observer_from_coordinate_camera(&m, &camera, &inner, NULL) == + OBSERVER_BUILD_OK, + "interior free-fall observer"); + GeodesicRayState state; + CHECK(trace_radial_dp(&source, &inner, 1.0, 1.0, &config, &state) == 0, + "interior trace"); + MetricData end_metric; + CHECK(spacetime_eval(&source, state.coordinate_time, state.x, &end_metric) == + SPACETIME_POINT_OK, + "interior end metric"); + CHECK(fabs(null_residual(&end_metric, state.Pi) - 1.0) < 1e-5, + "interior null constraint"); + } + + /* Tolerance tightening must improve (or preserve) the radial residual. */ + { + const GeodesicTraceConfig loose = dp_config(1e-5, 0.5, 1.0e6); + const GeodesicTraceConfig tight = dp_config(1e-9, 0.5, 1.0e6); + GeodesicRayState sl, st; + CHECK(trace_radial_dp(&source, &observer, 1.0, 2.0, &loose, &sl) == 0 && + trace_radial_dp(&source, &observer, 1.0, 2.0, &tight, &st) == 0, + "tolerance scan traces"); + const double slow = fabs(sl.x[0] - r0 + sl.coordinate_time); + const double stight = fabs(st.x[0] - r0 + st.coordinate_time); + CHECK(stight <= slow + 1e-9, "tighter tolerance no worse residual"); + CHECK(stight < 1e-6, "tight residual small"); + } + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 3. Over-large initial step: rejection without accepted-state damage */ +/* ------------------------------------------------------------------ */ + +static void test_over_large_initial_step(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0, + "create schwarzschild"); + const double r0 = 10.0; + MetricData metric; + CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) == + SPACETIME_POINT_OK, + "metric"); + ObserverState observer; + radial_static_observer(&metric, r0, &observer); + + const GeodesicTraceConfig small = dp_config(1e-9, 0.05, 1.0e6); + GeodesicRayState reference; + CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &small, &reference) == 0, + "reference small-step trace"); + + GeodesicTraceConfig big = dp_config(1e-9, 4.0, 1.0e6); + GeodesicRayState large; + CHECK(trace_radial_dp(&source, &observer, -1.0, 2.0, &big, &large) == 0, + "large initial step trace"); + CHECK(large.rejected_steps > 0, "large initial step was rejected"); + /* Comparing final states is the available evidence that rejected trials did + * not pollute the accepted trajectory; it is not a direct internal proof. */ + CHECK(fabs(large.coordinate_time - reference.coordinate_time) < 1e-12, + "large-step trace reaches the same time"); + for (int i = 0; i < 3; ++i) + CHECK(fabs(large.x[i] - reference.x[i]) < 1e-4, + "large-step result matches small-step reference"); + CHECK(fabs(large.log_alpha_p0 - reference.log_alpha_p0) < 1e-5, + "large-step L matches reference"); + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 4. Synthetic fixture: retryable stages, fatal points, hmin/maxreject */ +/* ------------------------------------------------------------------ */ + +/* Build a manual flat state at x = (x0, 0, 0) moving in +x in the past + * (Pi = -xhat), used by the fixture tests. */ +static void fixture_state(double x0, GeodesicRayState *state) { + memset(state, 0, sizeof *state); + state->x[0] = x0; + state->Pi[0] = -1.0; +} + +static void test_fixture_stage_retry(void) { + /* Step-size-dependent retryable stage failure: a thin forbidden wall lies + * across the outward path. The initial trial places a stage inside the + * wall (OUT_OF_DOMAIN), is rejected and shrunk; a smaller/reshaped step then + * jumps over the wall and the integration completes. */ + FixtureContext context; + memset(&context, 0, sizeof context); + context.has_domain = 1; + context.domain_lo = 0.9; + context.domain_hi = 0.95; + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + + GeodesicRayState state; + fixture_state(0.0, &state); + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-8, 1.15, 1.0e6); + state.next_step = config.coordinate_time_step; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, "wall slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult result = + geodesic_advance_past_ray(slab, &state, -2.0, &config, &out); + spacetime_free_slab(slab); + CHECK(result == GEODESIC_ADVANCE_ACTIVE, "wall overshoot eventually active"); + CHECK(fabs(state.coordinate_time + 2.0) < 1e-9, "wall reached target"); + CHECK(state.rejected_steps > 0, "wall overshoot caused a rejection"); + CHECK(fabs(state.x[0] - 2.0) < 1e-9, "wall integration stayed on the ray"); +} + +static void test_fixture_fatal_and_bounds(void) { + /* The initial accepted state itself is invalid: direct specific failure, + * one RHS call, no shrink. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_INVALID_METRIC; + context.bad_lo = -1.0e30; + context.bad_hi = 1.0e30; + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "invalid slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -0.5, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_INVALID_METRIC, + "initial invalid metric is a direct specific failure"); + CHECK(state.rhs_evaluations == 1u, "initial failure used exactly one RHS"); + CHECK(state.rejected_steps == 0u, "initial failure did not reject-retry"); + } + /* A later stage hits TIME_UNAVAILABLE: direct specific failure, no shrink. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE; + context.bad_lo = -0.3; + context.bad_hi = -0.01; + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "unavail slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -0.5, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED, + "stage TIME_UNAVAILABLE is a direct specific failure"); + CHECK(state.rhs_evaluations == 2u, + "stage failure was not retried with repeated shrink"); + CHECK(state.rejected_steps == 0u, "stage TIME_UNAVAILABLE not a reject"); + } + /* A later stage hits INTERNAL_ERROR: direct protocol failure. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_INTERNAL_ERROR; + context.bad_lo = -0.3; + context.bad_hi = -0.01; + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config = dp_config(1e-8, 0.5, 1.0e6); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "internal slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -0.5, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "stage INTERNAL_ERROR is a direct protocol failure"); + CHECK(state.rhs_evaluations == 2u, "internal error not retried"); + } + /* Retryable stage OUT_OF_DOMAIN that never clears: hmin and maxreject. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN; + context.bad_lo = -1.0e30; + context.bad_hi = -0.001; /* stage 1 (t = -h/5) is always in the window */ + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig hmin = dp_config(1e-8, 1.0, 1.0e6); + hmin.min_step = 0.1; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0, "hmin slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -10.0, &hmin, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.reason == RAY_REASON_INTEGRATION_ERROR, + "step below hmin is an integration error"); + CHECK(state.rejected_steps >= 1u, "hmin path rejected before failing"); + + FixtureContext context2; + memset(&context2, 0, sizeof context2); + context2.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN; + context2.bad_lo = -1.0e30; + context2.bad_hi = -0.001; + context2.radius = 1.0e9; + SpacetimeSource source2 = {.ops = &fixture_ops, .context = &context2}; + GeodesicRayState state2; + fixture_state(0.0, &state2); + GeodesicTraceConfig maxreject = dp_config(1e-8, 1.0, 1.0e6); + maxreject.min_step = 1e-12; + maxreject.consecutive_rejection_limit = 2; + MetricSlab *slab2 = NULL; + CHECK(spacetime_load_slab(&source2, 0.0, -10.0, &slab2) == 0, + "maxreject slab"); + RayEndpoint out2 = blank_endpoint(); + const GeodesicAdvanceResult r2 = + geodesic_advance_past_ray(slab2, &state2, -10.0, &maxreject, &out2); + spacetime_free_slab(slab2); + CHECK(r2 == GEODESIC_ADVANCE_FAILED && + out2.reason == RAY_REASON_OUT_OF_DOMAIN, + "consecutive rejection limit reports the specific stage reason"); + CHECK(state2.rejected_steps == 2u, "maxreject counted exactly two rejects"); + CHECK(state2.coordinate_time == 0.0, + "rejected trials did not advance the accepted time"); + } +} + +/* ------------------------------------------------------------------ */ +/* 5. Budget, explicit lookback, history exhaustion, resume */ +/* ------------------------------------------------------------------ */ + +static GeodesicRayState continuation_from(const RayEndpoint *e) { + GeodesicRayState s; + memset(&s, 0, sizeof s); + s.coordinate_time = e->stop_coordinate_time; + for (int i = 0; i < 3; ++i) { + s.x[i] = e->final_x[i]; + s.Pi[i] = e->final_Pi[i]; + } + s.log_alpha_p0 = e->final_log_alpha_p0; + s.log_alpha_p0_0 = e->final_log_alpha_p0_0; + s.steps = e->accepted_steps; + s.integration_start_time = e->integration_start_time; + s.next_step = e->next_step; + s.rejected_steps = e->rejected_steps; + s.rhs_evaluations = e->rhs_evaluations; + s.previous_rejected = e->previous_rejected; + return s; +} + +static void test_budget_lookback_and_resume(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); + const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0}); + const double direction[3] = {1.0, 0.0, 0.0}; + MetricData metric; + CHECK(spacetime_eval(&source, 0.0, (double[]){0.0, 0.0, 0.0}, &metric) == + SPACETIME_POINT_OK, + "metric"); + + /* Accepted-step budget exhaustion -> UNRESOLVED/BUDGET_EXHAUSTED. */ + { + GeodesicRayState state; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &state) == 0, + "budget init"); + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6); + config.max_steps = 3; + /* Keep three accepted steps well inside the r = 10 escape sphere. */ + config.max_step = 1.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -50.0, &slab) == 0, "budget slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -50.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_UNRESOLVED && + out.reason == RAY_REASON_BUDGET_EXHAUSTED, + "max_steps yields UNRESOLVED/BUDGET_EXHAUSTED"); + CHECK(state.steps == 3u, "budget consumed exactly max_steps"); + } + + /* Explicit lookback budget exhaustion -> UNRESOLVED, not history. */ + { + GeodesicRayState state; + CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction, + &state) == 0, + "lookback init"); + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 0.25); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -50.0, &slab) == 0, + "lookback slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -50.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_UNRESOLVED && + out.reason == RAY_REASON_BUDGET_EXHAUSTED, + "explicit lookback yields UNRESOLVED/BUDGET_EXHAUSTED"); + CHECK(fabs(state.coordinate_time + 0.25) < 1e-9, + "lookback stopped at the explicit budget"); + } + + /* A source slab data time hole is TIME_RANGE_EXHAUSTED, not UNRESOLVED. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE; + context.bad_lo = -1.0e30; + context.bad_hi = -0.3; + context.radius = 1.0e9; + SpacetimeSource fs = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config = dp_config(1e-9, 0.25, 1.0e6); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&fs, 0.0, -1.0, &slab) == 0, "hole slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE && + out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED, + "slab data hole is a specific INCOMPLETE reason, not UNRESOLVED"); + } + + /* Resume preserves L0, start time, counters and next step; an explicitly + * larger lookback budget continues the trace to escape without replay. */ + { + GeodesicTraceConfig first = dp_config(1e-9, 0.5, 1.0); + const RayEndpoint partial = + geodesic_trace_past(&source, &observer, direction, &first); + CHECK(partial.outcome == RAY_OUTCOME_UNRESOLVED && + partial.reason == RAY_REASON_BUDGET_EXHAUSTED, + "first lookback trace is unresolved"); + const double l0 = partial.final_log_alpha_p0_0; + const double start = partial.integration_start_time; + const unsigned long rhs_before = partial.rhs_evaluations; + + GeodesicRayState continuation = continuation_from(&partial); + GeodesicTraceConfig retry = dp_config(1e-9, 0.5, 100.0); + const RayEndpoint resumed = + geodesic_trace_past_from_state(&source, &continuation, &retry); + CHECK(resumed.outcome == RAY_OUTCOME_ESCAPED, "resumed trace escapes"); + CHECK(resumed.final_log_alpha_p0_0 == l0, "resume preserves L0"); + CHECK(resumed.integration_start_time == start, + "resume preserves integration start time"); + CHECK(resumed.rhs_evaluations > rhs_before, + "resume accumulates RHS cost without replaying from scratch"); + CHECK(resumed.accepted_steps >= partial.accepted_steps, + "resume keeps accepted step count"); + } + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 6. Escape event localization: flat analytic + strong-field compare */ +/* ------------------------------------------------------------------ */ + +static void test_flat_escape_localization(void) { + FixtureContext context; + memset(&context, 0, sizeof context); + context.has_domain = 0; + context.radius = 5.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + + /* Start inside the worldtube and integrate outward so the crossing is + * produced by the advance loop itself, not by the pre-route. */ + GeodesicRayState state; + fixture_state(2.0, &state); + GeodesicTraceConfig config = dp_config(1e-10, 0.5, 1.0e6); + state.next_step = config.coordinate_time_step; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, "escape slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -20.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED && out.end_id == 0, + "flat DP escape detected by the advance loop"); + CHECK(fabs(out.n_infinity[0] - 1.0) < 1e-6 && + fabs(out.n_infinity[1]) < 1e-6 && fabs(out.n_infinity[2]) < 1e-6, + "flat DP escape direction"); + const double expected_crossing = -(context.radius - 2.0); + CHECK(fabs(out.stop_coordinate_time - expected_crossing) < 1e-4, + "flat DP crossing time matches analytic root"); +} + +/* Event localization with a translated time origin. At t0 = 1e12 the local + * coordinate-time ULP is ~1.2e-4, so the crossing cannot be resolved below + * that; the old relative epsilon (= 1.0 at 1e12) skipped the subintegration + * and fabricated an off-trajectory endpoint. The endpoint must stay on the + * flat ray x(t) = 2 + (t0 - t) to a few ULP. */ +static void check_flat_crossing_at(double t0, double position_tol, + double time_tol) { + FixtureContext context; + memset(&context, 0, sizeof context); + context.radius = 5.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(2.0, &state); + state.coordinate_time = t0; + state.integration_start_time = t0; + GeodesicTraceConfig config = dp_config(1e-10, 0.5, 100.0); + config.max_step = 0.5; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, t0, t0 - 20.0, &slab) == 0, + "translated slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, t0 - 20.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "translated flat escape"); + CHECK(fabs(out.final_x[0] - 5.0) <= position_tol, + "translated crossing position matches analytic root"); + CHECK(fabs((t0 - out.stop_coordinate_time) - 3.0) <= time_tol, + "translated crossing time matches analytic root"); + CHECK(fabs(out.final_x[0] - 2.0 - (t0 - out.stop_coordinate_time)) <= + 2.0 * position_tol, + "translated endpoint lies on the flat ray"); +} + +static void test_event_time_translation(void) { + check_flat_crossing_at(0.0, 1e-6, 1e-6); + check_flat_crossing_at(1e12, 2e-3, 2e-3); +} + +/* A TIME_UNAVAILABLE window that the accepted main trial does not hit but a + * bisection subintegration does must surface as INCOMPLETE / + * TIME_RANGE_EXHAUSTED (the metric-integration reason), not as a worldtube + * protocol error, with the last trusted state restored and the failed + * subintegration RHS calls accumulated. */ +static void test_event_subintegration_time_hole(void) { + FixtureContext context; + memset(&context, 0, sizeof context); + context.radius = 2.7; + context.bad_status = SPACETIME_POINT_TIME_UNAVAILABLE; + /* The dense escape bracket re-localizes on the accepted trajectory; the + * bisection reaches t = -0.595 here, so the injected window must cover it. + * Only the subintegration sees the hole, not the accepted main trial. */ + context.bad_lo = -0.596; + context.bad_hi = -0.594; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(2.0, &state); + GeodesicTraceConfig config = dp_config(1e-10, 1.0, 100.0); + config.max_step = 1.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -2.0, &slab) == 0, + "event hole slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -2.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE, + "event subintegration hole fails as incomplete"); + CHECK(out.reason == RAY_REASON_TIME_RANGE_EXHAUSTED, + "event subintegration propagates TIME_RANGE_EXHAUSTED"); + CHECK(out.reason != RAY_REASON_PROTOCOL_ERROR, + "event integration reason is not a worldtube protocol error"); + CHECK(state.coordinate_time == 0.0 && out.stop_coordinate_time == 0.0, + "event hole restores the last trusted time"); + CHECK(out.final_x[0] == 2.0, + "event hole restores the last trusted position"); + CHECK(state.rhs_evaluations > 7u, + "event hole RHS includes the failed subintegration calls"); +} + +static void test_schwarzschild_dp_escape_matches_finish(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0, + "create schwarzschild"); + SpacetimeAsymptoticEnd end; + CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end"); + 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 = 5.0, + .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, + "canonical state"); + double n_analytic[3], freq_analytic; + CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_analytic, + &freq_analytic) == 0, + "analytic finish"); + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.x[0] = x[0]; + state.x[1] = x[1]; + state.x[2] = x[2]; + state.Pi[0] = Pi[0]; + state.Pi[1] = Pi[1]; + state.Pi[2] = Pi[2]; + state.log_alpha_p0 = log_alpha_p0; + state.log_alpha_p0_0 = log_alpha_p0; + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6); + state.next_step = config.coordinate_time_step; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0e6, &slab) == 0, "far slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0e6, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "strong-field DP integration escapes"); + const double angle = acos(fmax( + -1.0, fmin(1.0, dot3(n_analytic, out.n_infinity)))); + CHECK(angle < 1e-3, "DP escape direction matches analytic finish"); + CHECK(fabs(out.frequency_ratio / freq_analytic - 1.0) < 1e-3, + "DP escape frequency matches analytic finish"); + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 7. Legacy RK4 selection is unchanged (zero-initialized config) */ +/* ------------------------------------------------------------------ */ + +static void test_rk4_compatibility(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); + const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0}); + GeodesicTraceConfig config; + memset(&config, 0, sizeof config); + config.coordinate_time_step = 0.25; + config.max_steps = 100; + config.threshold.kind = THRESHOLD_DISABLED; + CHECK(config.stepper == GEODESIC_STEPPER_RK4, + "zero-initialized config selects RK4"); + const RayEndpoint out = + geodesic_trace_past(&source, &observer, (double[]){1.0, 0.0, 0.0}, + &config); + CHECK(out.outcome == RAY_OUTCOME_ESCAPED, "RK4 escape still works"); + CHECK(fabs(out.frequency_ratio - 1.0) < 1e-12, "RK4 flat frequency"); + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 8. RayPool batch scheduler: OMP determinism, single-trace agreement, */ +/* activation/control state and cross-slab reject accounting */ +/* ------------------------------------------------------------------ */ + +/* Sweep a pool from `top` downward until every ray terminates or the bottom + * bound is reached, activating pending rays in each slab. */ +static int pool_sweep(RayPool *pool, const SpacetimeSource *source, + const GeodesicTraceConfig *config, double top, + double bottom, double slab_duration) { + ray_pool_preroute(pool, source); + double slab_hi = top; + while (ray_pool_has_live(pool) && slab_hi > bottom) { + const double slab_lo = fmax(slab_hi - slab_duration, bottom); + MetricSlab *slab = NULL; + if (spacetime_load_slab(source, slab_hi, slab_lo, &slab)) + return -1; + ray_pool_activate_in_time_range(pool, slab); + ray_pool_advance_active(pool, slab, config); + spacetime_free_slab(slab); + slab_hi = slab_lo; + } + return 0; +} + +static void test_ray_pool_batch_matches_single(void) { + FixtureContext context; + memset(&context, 0, sizeof context); + context.radius = 50.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + const ObserverState observer = flat_observer_at((double[]){2.0, 0.0, 0.0}); + const double camera_directions[4][3] = {{1.0, 0.0, 0.0}, + {-1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, + {0.0, 0.0, 1.0}}; + const GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6); + RayEndpoint by_threads[2][4]; + for (int threads = 1; threads <= 2; ++threads) { + omp_set_num_threads(threads); + RayPool pool; + CHECK(ray_pool_init(&pool, 4) == 0, "batch pool init"); + for (int i = 0; i < 4; ++i) + CHECK(ray_pool_append(&pool, &observer, camera_directions[i], 0, + (size_t)i) == 0, + "batch pool append"); + ray_pool_preroute(&pool, &source); + for (int i = 0; i < 4; ++i) { + CHECK(pool.continuation[i] == 0, "new pool ray is not a continuation"); + CHECK(pool.observer[i] == &observer, "preroute keeps the observer"); + CHECK(pool.integration_start_time[i] == pool.activate_t[i], + "adaptive window starts at the activation time"); + CHECK(pool.next_step[i] == 0.0, + "first trial step is deferred to the trace config"); + CHECK(pool.rejected_steps[i] == 0 && pool.rhs_evaluations[i] == 0, + "new pool ray starts with zero adaptive cost"); + } + CHECK(pool_sweep(&pool, &source, &config, 0.0, -1000.0, 10.0) == 0, + "batch pool sweep"); + for (int i = 0; i < 4; ++i) { + CHECK(pool.status[i] == RAY_POOL_TERMINATED, "batch ray terminated"); + CHECK(pool.endpoint[i].outcome == RAY_OUTCOME_ESCAPED, + "batch ray escapes"); + CHECK(pool.steps[i] > 0, "batch ray accepted steps"); + by_threads[threads - 1][i] = pool.endpoint[i]; + } + ray_pool_destroy(&pool); + } + omp_set_num_threads(1); + for (int i = 0; i < 4; ++i) { + for (int axis = 0; axis < 3; ++axis) + CHECK(fabs(by_threads[0][i].n_infinity[axis] - + by_threads[1][i].n_infinity[axis]) < 1e-12, + "OMP1/OMP2 escape direction identical"); + CHECK(fabs(by_threads[0][i].stop_coordinate_time - + by_threads[1][i].stop_coordinate_time) < 1e-12, + "OMP1/OMP2 crossing time identical"); + } + for (int i = 0; i < 4; ++i) { + const RayEndpoint single = + geodesic_trace_past(&source, &observer, camera_directions[i], &config); + CHECK(single.outcome == RAY_OUTCOME_ESCAPED, "single trace escapes"); + for (int axis = 0; axis < 3; ++axis) + CHECK(fabs(single.n_infinity[axis] - by_threads[0][i].n_infinity[axis]) < + 1e-3, + "pool matches single-trace escape direction"); + CHECK(fabs(single.stop_coordinate_time - + by_threads[0][i].stop_coordinate_time) < 1e-2, + "pool matches single-trace crossing time"); + } +} + +static void test_ray_pool_continuation_state(void) { + FixtureContext context; + memset(&context, 0, sizeof context); + context.radius = 1.0e9; /* no escape inside the traced window */ + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0}); + GeodesicTraceConfig first = dp_config(1e-9, 0.5, 1.0e6); + first.max_steps = 3; + first.max_step = 1.0; + const RayEndpoint partial = geodesic_trace_past( + &source, &observer, (double[]){-1.0, 0.0, 0.0}, &first); + CHECK(partial.outcome == RAY_OUTCOME_UNRESOLVED && + partial.reason == RAY_REASON_BUDGET_EXHAUSTED, + "partial trace is budget-unresolved"); + + GeodesicRayState state = continuation_from(&partial); + const double l0 = state.log_alpha_p0_0; + const double start = state.integration_start_time; + const double t_before = state.coordinate_time; + const unsigned long rhs_before = state.rhs_evaluations; + + RayPool pool; + CHECK(ray_pool_init(&pool, 1) == 0, "continuation pool init"); + CHECK(ray_pool_append_continuation_state(&pool, 0, 0, &state, 6, + partial.lookback_limit) == 0, + "append continuation state"); + CHECK(pool.continuation[0] == 1, "continuation is flagged"); + CHECK(pool.observer[0] == NULL, "continuation carries no camera observer"); + CHECK(pool.integration_start_time[0] == start && + pool.log_alpha_p0_0[0] == l0 && + pool.rejected_steps[0] == state.rejected_steps && + pool.rhs_evaluations[0] == rhs_before && + pool.next_step[0] == state.next_step, + "continuation copies the full adaptive state"); + ray_pool_preroute(&pool, &source); + CHECK(pool.status[0] == RAY_POOL_PENDING && pool.observer[0] == NULL, + "continuation skips preroute"); + CHECK(pool.integration_start_time[0] == start, + "preroute does not touch continuation control state"); + + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0, + "continuation slab"); + ray_pool_activate_in_time_range(&pool, slab); + CHECK(pool.status[0] == RAY_POOL_ACTIVE && pool.steps[0] == state.steps, + "activation preserves the continuation step count"); + ray_pool_advance_active(&pool, slab, &first); + spacetime_free_slab(slab); + + CHECK(pool.steps[0] == 6u, "continuation reached the new step budget"); + CHECK(pool.endpoint[0].outcome == RAY_OUTCOME_UNRESOLVED, + "continuation is still budget-unresolved"); + CHECK(pool.t[0] < t_before, "continuation moved further into the past"); + CHECK(pool.integration_start_time[0] == start && + pool.log_alpha_p0_0[0] == l0, + "continuation preserves window start and L0"); + CHECK(pool.rhs_evaluations[0] > rhs_before, + "continuation accumulates RHS without replay"); + CHECK(pool.rhs_evaluations[0] == + 7ul * ((unsigned long)pool.steps[0] + + (unsigned long)pool.rejected_steps[0]), + "pool RHS count matches full DP trials across the resume"); + ray_pool_destroy(&pool); +} + +static void test_ray_pool_cross_slab_reject(void) { + /* A thin forbidden wall lies across the outward path: the first trial is + * rejected and shrunk. Splitting the sweep across slabs verifies that the + * accumulated rejection/RHS cost and the accepted state survive both the + * slab boundary and the pool gather/scatter. */ + FixtureContext context; + memset(&context, 0, sizeof context); + context.has_domain = 1; + context.domain_lo = 0.9; + context.domain_hi = 0.95; + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0}); + RayPool pool; + CHECK(ray_pool_init(&pool, 1) == 0, "reject pool init"); + /* A flat camera direction n gives Pi = -n, so n = +xhat sends the past ray + * toward increasing x, across the forbidden wall. */ + CHECK(ray_pool_append(&pool, &observer, (double[]){1.0, 0.0, 0.0}, 0, 0) == + 0, + "reject pool append"); + const GeodesicTraceConfig config = dp_config(1e-8, 1.15, 1.0e6); + ray_pool_preroute(&pool, &source); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.5, &slab) == 0, "reject slab 1"); + ray_pool_activate_in_time_range(&pool, slab); + ray_pool_advance_active(&pool, slab, &config); + spacetime_free_slab(slab); + const unsigned int rejected_first = pool.rejected_steps[0]; + const unsigned long rhs_first = pool.rhs_evaluations[0]; + CHECK(rejected_first > 0, "wall overshoot rejected in the first slab"); + CHECK(rhs_first > 0, "first slab spent RHS calls"); + CHECK(spacetime_load_slab(&source, -1.5, -2.0, &slab) == 0, "reject slab 2"); + ray_pool_activate_in_time_range(&pool, slab); + ray_pool_advance_active(&pool, slab, &config); + spacetime_free_slab(slab); + CHECK(fabs(pool.t[0] + 2.0) < 1e-9, "reject pool reached the target time"); + CHECK(fabs(pool.x0[0] - 2.0) < 1e-9, "reject pool stayed on the ray"); + CHECK(pool.steps[0] > 0, "reject run accepted steps"); + CHECK(pool.rejected_steps[0] >= rejected_first, + "rejection count survives the slab boundary"); + CHECK(pool.rhs_evaluations[0] > rhs_first, + "RHS cost accumulates across slabs"); + /* Every accepted trial costs 7 RHS; a rejected trial costs at least the + * initial evaluation plus the failing stage. */ + CHECK(pool.rhs_evaluations[0] >= + 7ul * (unsigned long)pool.steps[0] + + 2ul * (unsigned long)pool.rejected_steps[0], + "cross-slab reject/RHS accounting is consistent"); + ray_pool_destroy(&pool); +} + +/* ------------------------------------------------------------------ */ +/* 9. Dense event layer: polynomial roots, multi-end order, segments, */ +/* endpoint/tangent roots, and threshold-versus-escape ordering. */ +/* ------------------------------------------------------------------ */ + +/* Focused hook into the production dense-event polynomial root finder. */ +extern int geodesic_event_polynomial_exit(const double *coeff, int degree, + double *theta, double *lo, + double *hi); +extern int geodesic_event_worldtube_dense_probe(const MetricSlab *slab, + const GeodesicTraceConfig *config, + SpacetimeEndId end_id, + GeodesicRayState *state, + double left, double theta, + double *dense_value, + double *actual_value); + +static void test_event_polynomial_roots(void) { + double theta, lo, hi; + /* inside -> outside -> inside: the earliest exit at the FIRST root. The + * production root isolator must not return the later re-entry. */ + { + const double c[3] = {-0.1875, 1.0, -1.0}; /* -theta^2+theta-0.1875 */ + CHECK(geodesic_event_polynomial_exit(c, 2, &theta, &lo, &hi) == 1, + "in-out-in polynomial has an exit"); + CHECK(fabs(theta - 0.25) < 1e-9, "earliest exit is the first root"); + CHECK(lo <= 1e-12 && hi > 0.25 && hi < 0.75, + "exit bracket stops before the re-entry"); + } + /* Tangent (even multiplicity, no sign change) is not an escape. */ + { + const double c[3] = {-0.25, 1.0, -1.0}; /* -(theta-0.5)^2 */ + CHECK(geodesic_event_polynomial_exit(c, 2, &theta, &lo, &hi) == 0, + "tangent touch is not an escape"); + } + /* A root exactly at the step endpoint is left for the following step. */ + { + const double c[2] = {-1.0, 1.0}; /* theta - 1 */ + CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 0, + "root at theta == 1 is not an in-step exit"); + } + /* A boundary start that immediately moves outside is an exit at theta 0. */ + { + const double c[2] = {0.0, 1.0}; /* theta */ + CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 1, + "boundary start moving outside is an exit"); + CHECK(theta <= 1e-12, "boundary exit is at theta 0"); + } + /* outside -> inside (entry) is not an escape. */ + { + const double c[2] = {1.0, -2.0}; /* 1 - 2 theta */ + CHECK(geodesic_event_polynomial_exit(c, 1, &theta, &lo, &hi) == 0, + "entry is not an escape"); + } +} + +/* Flat worldtube fixture with constant-velocity ends, optional motion + * segments (piecewise-constant velocity/radius_rate) and several ends. */ +#define EVENT_ENDS 2 +typedef struct { + size_t end_count; + SpacetimeEndId id[EVENT_ENDS]; + double center[EVENT_ENDS][3]; + double velocity[EVENT_ENDS][3]; + double radius_a[EVENT_ENDS]; + double radius_rate_a[EVENT_ENDS]; + double radius_b[EVENT_ENDS]; + double radius_rate_b[EVENT_ENDS]; + double segment_t[EVENT_ENDS]; + double t_ref[EVENT_ENDS]; /* center reference time for the moving end */ + int has_segment[EVENT_ENDS]; + int velocity_constant[EVENT_ENDS]; +} EventContext; + +static SpacetimePointStatus event_eval(const SpacetimeSource *source, double t, + const double x[3], MetricData *metric) { + (void)source; + (void)t; + (void)x; + *metric = flat_metric(); + return SPACETIME_POINT_OK; +} + +static SpacetimeRayStatus event_classify(const SpacetimeSource *source, + double t, const double x[3]) { + (void)source; + (void)t; + (void)x; + return SPACETIME_RAY_ACTIVE; +} + +static size_t event_end_count(const SpacetimeSource *source) { + return ((const EventContext *)source->context)->end_count; +} + +static int event_end(const SpacetimeSource *source, size_t index, + SpacetimeAsymptoticEnd *out) { + const EventContext *c = source->context; + if (index >= c->end_count) + return -1; + *out = (SpacetimeAsymptoticEnd){ + .end_id = c->id[index], + .exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI, + .mass = 0.0, + .frame_origin = {0.0, 0.0, 0.0}, + .frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + return 0; +} + +static int event_worldtube(const SpacetimeSource *source, + SpacetimeEndId end_id, double t, + SpacetimeEscapeWorldtubeSample *out) { + const EventContext *c = source->context; + for (size_t i = 0; i < c->end_count; ++i) { + if (c->id[i] != end_id) + continue; + double radius = c->radius_a[i]; + double radius_rate = c->radius_rate_a[i]; + if (c->has_segment[i] && t < c->segment_t[i]) { + radius = c->radius_b[i] + c->radius_rate_b[i] * (t - c->segment_t[i]); + radius_rate = c->radius_rate_b[i]; + } + const double dt = t - c->t_ref[i]; + *out = (SpacetimeEscapeWorldtubeSample){ + .center = {c->center[i][0] + c->velocity[i][0] * dt, + c->center[i][1] + c->velocity[i][1] * dt, + c->center[i][2] + c->velocity[i][2] * dt}, + .velocity = {c->velocity[i][0], c->velocity[i][1], c->velocity[i][2]}, + .radius = radius, + .radius_rate = radius_rate, + .velocity_constant = c->velocity_constant[i], + .valid = 1}; + return 0; + } + return -1; +} + +static double event_next_segment(const SpacetimeSource *source, + SpacetimeEndId end_id, double t) { + const EventContext *c = source->context; + for (size_t i = 0; i < c->end_count; ++i) + if (c->id[i] == end_id && c->has_segment[i] && t > c->segment_t[i]) + return c->segment_t[i]; + return NAN; +} + +static void event_destroy(SpacetimeSource *source) { + source->context = NULL; + source->ops = NULL; +} + +static const SpacetimeOps event_ops = { + .eval = event_eval, + .classify = event_classify, + .asymptotic_end_count = event_end_count, + .asymptotic_end = event_end, + .escape_worldtube_sample = event_worldtube, + .escape_worldtube_next_segment = event_next_segment, + .destroy = event_destroy, +}; + +/* Two ends at the same center: end id 0 is the smaller (nearer) sphere, so it + * is exited first no matter which descriptor position it occupies. */ +static void test_event_two_ends_order(void) { + for (int swap = 0; swap < 2; ++swap) { + EventContext c; + memset(&c, 0, sizeof c); + c.end_count = 2; + c.velocity_constant[0] = c.velocity_constant[1] = 1; + const SpacetimeEndId near_id = 3, far_id = 7; + const double near_radius = 3.0, far_radius = 5.0; + if (!swap) { + c.id[0] = near_id; + c.radius_a[0] = near_radius; + c.id[1] = far_id; + c.radius_a[1] = far_radius; + } else { + c.id[0] = far_id; + c.radius_a[0] = far_radius; + c.id[1] = near_id; + c.radius_a[1] = near_radius; + } + SpacetimeSource source = {.ops = &event_ops, .context = &c}; + GeodesicRayState state; + fixture_state(2.0, &state); + GeodesicTraceConfig config = dp_config(1e-10, 10.0, 1.0e6); + config.max_step = 10.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, "two-end slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -20.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "two-end escape terminates"); + CHECK(out.end_id == near_id, + "earlier end wins regardless of descriptor order"); + CHECK(fabs(out.stop_coordinate_time + 1.0) < 1e-6, + "two-end crossing is the nearest surface"); + } +} + +/* Piecewise-constant worldtube: the step must be clamped to the segment + * boundary or the constant-velocity polynomial extrapolates the wrong radius + * and predicts the wrong crossing. */ +static void test_event_segmented_worldtube(void) { + EventContext c; + memset(&c, 0, sizeof c); + c.end_count = 1; + c.id[0] = 0; + c.velocity_constant[0] = 1; + c.radius_a[0] = 5.0; + c.radius_rate_a[0] = 0.0; + c.radius_b[0] = 5.0; + c.radius_rate_b[0] = 1.0; + c.segment_t[0] = -2.0; + c.has_segment[0] = 1; + SpacetimeSource source = {.ops = &event_ops, .context = &c}; + GeodesicRayState state; + fixture_state(2.0, &state); + GeodesicTraceConfig config = dp_config(1e-10, 4.0, 1.0e6); + config.max_step = 4.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, + "segment slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -20.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "segmented worldtube escape"); + CHECK(fabs(out.stop_coordinate_time + 2.5) < 1e-6, + "segmented crossing uses the second segment"); +} + +/* Arbitrary accelerated worldtubes are explicitly unsupported rather than + * routed with a coarse sign search that can miss a narrow first entry. */ +static void test_event_nonconstant_unsupported(void) { + EventContext c; + memset(&c, 0, sizeof c); + c.end_count = 1; + c.id[0] = 0; + c.radius_a[0] = 5.0; + c.velocity_constant[0] = 0; /* arbitrary acceleration */ + SpacetimeSource source = {.ops = &event_ops, .context = &c}; + + /* Pre-route from outside must report UNSUPPORTED, not a fabricated miss. */ + const ObserverState outside = flat_observer_at((double[]){10.0, 0.0, 0.0}); + AsymptoticRoute route; + CHECK(asymptotic_route_camera(&source, &outside, (double[]){-1.0, 0.0, 0.0}, + &route) == ASYMPTOTIC_UNSUPPORTED, + "nonconstant preroute is unsupported"); + + /* An inside state that reaches the advance loop is rejected there too. */ + GeodesicRayState state; + fixture_state(1.0, &state); + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 1.0e6); + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0, + "nonconstant slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -5.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE && + out.reason == RAY_REASON_UNSUPPORTED, + "nonconstant advance is an explicit unsupported failure"); +} + +/* Synthetic curved metric with alpha = 1 - k x. The Eulerian energy + * L = ln(alpha p^0) grows along the past-directed ray, so a threshold crossing + * and a worldtube escape can be placed in the same accepted step. */ +typedef struct { + double k; + double radius; + int has_bad; + double bad_lo, bad_hi; +} AlphaContext; + +static SpacetimePointStatus alpha_eval(const SpacetimeSource *source, double t, + const double x[3], MetricData *metric) { + const AlphaContext *c = source->context; + if (c->has_bad && t >= c->bad_lo && t <= c->bad_hi) + return SPACETIME_POINT_OUT_OF_DOMAIN; + const double alpha = 1.0 - c->k * x[0]; + if (!(alpha > 0.0)) + return SPACETIME_POINT_INVALID_METRIC; + *metric = flat_metric(); + metric->alpha = alpha; + metric->d_alpha[0] = -c->k; + return SPACETIME_POINT_OK; +} + +static size_t alpha_end_count(const SpacetimeSource *source) { + (void)source; + return 1; +} + +static int alpha_end(const SpacetimeSource *source, size_t index, + SpacetimeAsymptoticEnd *out) { + (void)source; + if (index != 0) + return -1; + *out = (SpacetimeAsymptoticEnd){ + .end_id = 0, + .exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI, + .mass = 0.0, + .frame_origin = {0.0, 0.0, 0.0}, + .frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + return 0; +} + +static int alpha_worldtube(const SpacetimeSource *source, + SpacetimeEndId end_id, double t, + SpacetimeEscapeWorldtubeSample *out) { + const AlphaContext *c = source->context; + (void)t; + if (end_id != 0) + return -1; + *out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0}, + .velocity = {0.0, 0.0, 0.0}, + .radius = c->radius, + .radius_rate = 0.0, + .velocity_constant = 1, + .valid = 1}; + return 0; +} + +static const SpacetimeOps alpha_ops = { + .eval = alpha_eval, + .classify = event_classify, + .asymptotic_end_count = alpha_end_count, + .asymptotic_end = alpha_end, + .escape_worldtube_sample = alpha_worldtube, + .destroy = event_destroy, +}; + +static void alpha_state(double x0, double k, GeodesicRayState *state) { + memset(state, 0, sizeof *state); + state->x[0] = x0; + state->Pi[0] = -1.0; + state->log_alpha_p0 = log(1.0 - k * x0); + state->log_alpha_p0_0 = state->log_alpha_p0; +} + +/* Threshold and escape in the same accepted step: the earlier event wins. */ +static void test_event_threshold_order(void) { + struct { + double radius; + RayOutcome outcome; + const char *label; + } cases[2] = {{0.5, RAY_OUTCOME_DARK, "threshold first"}, + {0.3, RAY_OUTCOME_ESCAPED, "escape first"}}; + for (int i = 0; i < 2; ++i) { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = cases[i].radius; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(1e-4, 1.0, 1.0e6); + config.max_step = 1.0; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = 0.35, + .policy_version = 1}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, + "threshold slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -3.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == cases[i].outcome, + cases[i].label); + if (out.outcome == RAY_OUTCOME_DARK) { + CHECK(out.accepted_steps == 1u, + "threshold and escape were compared within one accepted step"); + CHECK(out.reason == RAY_REASON_REDSHIFT_LIMIT, "dark reason"); + CHECK(out.threshold_value >= 0.35 && + out.threshold_value < 0.35 + 0.05, + "dark records the actual threshold value"); + } + } +} + +/* A trial whose stages cross the threshold but is rejected must not produce a + * dark terminal. Here the long first trial hits a forbidden window, is + * rejected and shrunk to a step that stays below the threshold; with a + * one-step budget the ray ends UNRESOLVED, not DARK. */ +static void test_event_rejected_stage_threshold(void) { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = 10.0; + c.has_bad = 1; + c.bad_lo = -0.81; + c.bad_hi = -0.79; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(1e-9, 1.0, 1.0e6); + config.max_step = 1.0; + config.max_steps = 1; + /* Threshold crossing near s = 0.8, well beyond the shrunk accepted step. */ + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = 0.8, + .policy_version = 1}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -2.0, &slab) == 0, + "reject threshold slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -2.0, &config, &out); + spacetime_free_slab(slab); + CHECK(state.rejected_steps > 0u, "long trial was rejected"); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_UNRESOLVED, + "rejected trial does not create DARK"); + CHECK(out.reason == RAY_REASON_BUDGET_EXHAUSTED, + "unresolved reason is budget exhaustion"); +} + +/* ------------------------------------------------------------------ */ +/* 10. RK4 real RHS accounting and explicit unknown-stepper rejection */ +/* ------------------------------------------------------------------ */ + +static void test_rk4_rhs_cost(void) { + /* Four fixed RK4 steps over a finite interval cost exactly four RHS each. */ + { + SpacetimeSource source = {0}; + CHECK(spacetime_create_minkowski(&source, 1.0e9) == 0, "create minkowski"); + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config; + memset(&config, 0, sizeof config); + config.stepper = GEODESIC_STEPPER_RK4; + config.coordinate_time_step = 0.25; + config.max_steps = 100; + config.threshold.kind = THRESHOLD_DISABLED; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "rk4 slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_ACTIVE, "rk4 finite interval is active"); + CHECK(state.steps == 4u, "rk4 took four accepted steps"); + CHECK(state.rhs_evaluations == 16u, "rk4 four steps cost sixteen RHS"); + CHECK(state.rejected_steps == 0u, "rk4 never rejects"); + spacetime_destroy(&source); + } + /* A stage failure costs the real number of evaluations performed. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.bad_status = SPACETIME_POINT_OUT_OF_DOMAIN; + context.bad_lo = -1.0e30; + context.bad_hi = -0.01; /* stage 1 at t = -0.05 fails, the initial is OK */ + context.radius = 1.0e9; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(0.0, &state); + GeodesicTraceConfig config; + memset(&config, 0, sizeof config); + config.stepper = GEODESIC_STEPPER_RK4; + config.coordinate_time_step = 0.1; + config.max_steps = 10; + config.threshold.kind = THRESHOLD_DISABLED; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, + "rk4 failure slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE && + out.reason == RAY_REASON_OUT_OF_DOMAIN, + "rk4 failed stage reports the point status"); + CHECK(state.rhs_evaluations == 2u, + "rk4 failed stage-1 cost is the real two RHS"); + CHECK(state.rejected_steps == 0u, "rk4 failure is not a rejection"); + } + /* Escape localization adds its own real RHS cost on top of the steps. */ + { + FixtureContext context; + memset(&context, 0, sizeof context); + context.radius = 5.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &context}; + GeodesicRayState state; + fixture_state(2.0, &state); + GeodesicTraceConfig config; + memset(&config, 0, sizeof config); + config.stepper = GEODESIC_STEPPER_RK4; + config.coordinate_time_step = 0.5; + config.max_steps = 100; + config.threshold.kind = THRESHOLD_DISABLED; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -20.0, &slab) == 0, + "rk4 escape slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -20.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "rk4 escape still terminates"); + CHECK(out.accepted_steps > 0u && + out.rhs_evaluations > 4ul * out.accepted_steps, + "rk4 escape localization adds real RHS cost"); + } +} + +static void test_unknown_stepper_rejected(void) { + SpacetimeSource source = {0}; + CHECK(spacetime_create_minkowski(&source, 10.0) == 0, "create minkowski"); + const ObserverState observer = flat_observer_at((double[]){0.0, 0.0, 0.0}); + GeodesicTraceConfig config; + memset(&config, 0, sizeof config); + config.stepper = (GeodesicStepper)99; + config.coordinate_time_step = 0.25; + config.max_steps = 10; + config.threshold.kind = THRESHOLD_DISABLED; + const RayEndpoint traced = geodesic_trace_past( + &source, &observer, (double[]){1.0, 0.0, 0.0}, &config); + CHECK(traced.outcome == RAY_OUTCOME_INCOMPLETE && + traced.reason == RAY_REASON_PROTOCOL_ERROR, + "trace_past rejects an unknown stepper"); + + GeodesicRayState state; + fixture_state(0.0, &state); + const RayEndpoint resumed = + geodesic_trace_past_from_state(&source, &state, &config); + CHECK(resumed.outcome == RAY_OUTCOME_INCOMPLETE && + resumed.reason == RAY_REASON_PROTOCOL_ERROR, + "trace_past_from_state rejects an unknown stepper"); + + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "unknown slab"); + RayEndpoint advanced = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &advanced); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + advanced.outcome == RAY_OUTCOME_INCOMPLETE && + advanced.reason == RAY_REASON_PROTOCOL_ERROR, + "advance rejects an unknown stepper"); + spacetime_destroy(&source); +} + +/* ------------------------------------------------------------------ */ +/* 11. Production advance on a curved synthetic metric: real single- */ +/* step inside->outside->inside, tangent, and endpoint roots. */ +/* */ +/* gamma = I, alpha = 1, K = 0, beta(t) = (3 - 8 (t0 - t), 0, 0) with */ +/* no spatial beta derivative. This is a flat metric in accelerating */ +/* translated coordinates: Pi and L are constant, the past spatial path */ +/* is x(s) = 4 s (1 - s) for s = t0 - t in [0, 1]. It is a geometric */ +/* numerical fixture for the production event layer, not a physical */ +/* outer-region model; the terminal Minkowski conversion only reads the */ +/* legal local metric at the crossing. */ +/* ------------------------------------------------------------------ */ + +typedef struct { + double t0; + double radius; +} CurvedContext; + +static SpacetimePointStatus curved_eval(const SpacetimeSource *source, + double t, const double x[3], + MetricData *metric) { + const CurvedContext *c = source->context; + (void)x; + *metric = flat_metric(); + metric->beta[0] = 3.0 - 8.0 * (c->t0 - t); + return SPACETIME_POINT_OK; +} + +static size_t curved_end_count(const SpacetimeSource *source) { + (void)source; + return 1; +} + +static int curved_end(const SpacetimeSource *source, size_t index, + SpacetimeAsymptoticEnd *out) { + (void)source; + if (index != 0) + return -1; + *out = (SpacetimeAsymptoticEnd){ + .end_id = 0, + .exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI, + .mass = 0.0, + .frame_origin = {0.0, 0.0, 0.0}, + .frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + return 0; +} + +static int curved_worldtube(const SpacetimeSource *source, + SpacetimeEndId end_id, double t, + SpacetimeEscapeWorldtubeSample *out) { + const CurvedContext *c = source->context; + (void)t; + if (end_id != 0) + return -1; + *out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0}, + .velocity = {0.0, 0.0, 0.0}, + .radius = c->radius, + .radius_rate = 0.0, + .velocity_constant = 1, + .valid = 1}; + return 0; +} + +static const SpacetimeOps curved_ops = { + .eval = curved_eval, + .classify = event_classify, + .asymptotic_end_count = curved_end_count, + .asymptotic_end = curved_end, + .escape_worldtube_sample = curved_worldtube, + .destroy = event_destroy, +}; + +/* Null past state: Pi = (-1, 0, 0) has gamma^{ij} Pi_i Pi_j = 1. */ +static void curved_state(GeodesicRayState *state) { + memset(state, 0, sizeof *state); + state->Pi[0] = -1.0; +} + +/* The earliest exit of the inside->outside->inside path is s = 0.1464466. */ +static double curved_first_exit_s(void) { return (1.0 - sqrt(0.5)) / 2.0; } + +static void test_event_curved_in_out_in(void) { + CurvedContext c = {.t0 = 0.0, .radius = 0.5}; + SpacetimeSource source = {.ops = &curved_ops, .context = &c}; + GeodesicRayState state; + curved_state(&state); + const double expected_s = curved_first_exit_s(); + GeodesicTraceConfig config = dp_config(1e-10, 1.0, 1.0e6); + config.max_step = 1.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "curved slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "curved in-out-in escapes at the first exit"); + CHECK(out.end_id == 0, "curved escape end id"); + CHECK(fabs(out.final_x[0] - 0.5) < 1e-6, + "curved earliest exit position"); + CHECK(fabs(out.stop_coordinate_time + expected_s) < 1e-6, + "curved earliest exit time"); + MetricData metric; + CHECK(spacetime_eval(&source, 0.0, state.x, &metric) == SPACETIME_POINT_OK && + fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12, + "curved fixture state is null"); +} + +static void test_event_curved_tangent(void) { + /* The path peaks at x = 1, exactly touching the R = 1 sphere at s = 0.5. + * The tangent touch must not terminate the ray. */ + CurvedContext c = {.t0 = 0.0, .radius = 1.0}; + SpacetimeSource source = {.ops = &curved_ops, .context = &c}; + GeodesicRayState state; + curved_state(&state); + GeodesicTraceConfig config = dp_config(1e-10, 1.0, 1.0e6); + config.max_step = 1.0; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "tangent slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_ACTIVE, + "tangent touch does not escape"); + CHECK(state.coordinate_time == -1.0, + "tangent trace reached the slab boundary"); +} + +static void test_event_curved_endpoint_root(void) { + /* Truncate the step exactly at the first exit so F == 0 lands on the step + * endpoint. The in-step enumeration must not accept it; the following step + * starts at the boundary root and escapes reliably. */ + const double expected_s = curved_first_exit_s(); + CurvedContext c = {.t0 = 0.0, .radius = 0.5}; + SpacetimeSource source = {.ops = &curved_ops, .context = &c}; + GeodesicRayState state; + curved_state(&state); + GeodesicTraceConfig config = dp_config(1e-12, expected_s, 1.0e6); + config.max_step = expected_s; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, + "endpoint slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "endpoint root escapes on a later step"); + CHECK(out.accepted_steps == 2u, + "endpoint root is detected on the following boundary step"); + CHECK(fabs(out.stop_coordinate_time + expected_s) < 1e-6, + "endpoint root crossing time"); + CHECK(fabs(out.final_x[0] - 0.5) < 1e-6, "endpoint root position"); +} + +/* LOG_P0 is not polynomial in the dense output, so under DP54 it is still only + * checked on trusted accepted states; it is not the CLI default and needs no + * dense candidate. */ +static void test_event_log_p0_accepted_state(void) { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = 10.0; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(1e-6, 0.25, 1.0e6); + config.max_step = 0.25; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_P0, + .value = 0.2, + .policy_version = 1}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "log_p0 slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_DARK, + "LOG_P0 truncates on an accepted state"); + CHECK(out.reason == RAY_REASON_REDSHIFT_LIMIT, "LOG_P0 dark reason"); + CHECK(out.threshold_value >= 0.2, + "LOG_P0 records the reached monitored value"); +} + +/* ------------------------------------------------------------------ */ +/* 12. Review-fix regressions: representable step times, absorbed */ +/* endpoint roots, and actual-time escape/threshold ordering. */ +/* ------------------------------------------------------------------ */ + +/* A nominal step must commit the representable interval it actually + * integrated: x must equal the elapsed coordinate time to local ULP even when + * t + h rounds badly. */ +static void test_event_step_time_precision(void) { + const double origins[3] = {0.0, 1.0e12, 1.0e14}; + for (int i = 0; i < 3; ++i) { + FixtureContext c; + memset(&c, 0, sizeof c); + c.radius = 1.0e6; + SpacetimeSource source = {.ops = &fixture_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.coordinate_time = origins[i]; + state.integration_start_time = origins[i]; + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-9, 0.1, 10.0); + config.max_step = 0.1; + config.max_steps = 10; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, origins[i], origins[i] - 2.0, &slab) == 0, + "time-precision slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = geodesic_advance_past_ray( + slab, &state, origins[i] - 2.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_UNRESOLVED, + "ten-step budget is unresolved"); + CHECK(state.steps == 10u, "ten accepted steps"); + const double elapsed = origins[i] - state.coordinate_time; + CHECK(fabs(state.x[0] - elapsed) < 1e-10, + "committed position matches elapsed coordinate time"); + MetricData metric; + CHECK(spacetime_eval(&source, state.coordinate_time, state.x, &metric) == + SPACETIME_POINT_OK && + fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12, + "time-precision fixture stays null"); + } +} + +/* A crossing whose dense root is absorbed at theta == 1 (F_after strictly + * positive) must still escape via the actual-trajectory localizer. */ +static void test_event_absorbed_endpoint_exit(void) { + double radius = 1.0; + for (int j = 0; j < 3; ++j) + radius = nextafter(radius, 0.0); + FixtureContext c; + memset(&c, 0, sizeof c); + c.radius = radius; + SpacetimeSource source = {.ops = &fixture_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-9, 1.0, 10.0); + config.max_step = 1.0; + config.max_steps = 4; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0, + "absorbed-root slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -5.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "absorbed endpoint root still escapes"); + CHECK(fabs(out.final_x[0] - radius) <= 4.0 * DBL_EPSILON, + "absorbed-root crossing stays on the surface"); +} + +/* A start within the local geometric ULP outside the boundary is a boundary + * state and must escape when it moves outside. */ +static void test_event_boundary_roundoff_exit(void) { + FixtureContext c; + memset(&c, 0, sizeof c); + c.radius = 1.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.x[0] = nextafter(1.0, 2.0); + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 10.0); + config.max_step = 0.5; + config.max_steps = 4; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0, + "roundoff slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -5.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "boundary roundoff start escapes outward"); +} + +/* A single-end state clearly outside its worldtube is a protocol error, not an + * unbounded search that can only end as budget exhaustion. */ +static void test_event_clearly_outside_protocol(void) { + FixtureContext c; + memset(&c, 0, sizeof c); + c.radius = 1.0; + SpacetimeSource source = {.ops = &fixture_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.x[0] = 2.0; + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-9, 0.5, 10.0); + config.max_step = 0.5; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0, + "outside slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -5.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE && + out.reason == RAY_REASON_PROTOCOL_ERROR, + "clearly-outside single end is a protocol error"); +} + +/* Escape and threshold are both localized on the actual trajectory and ordered + * by real coordinate time; the ODE tolerance is not a time tie. */ +static void test_event_threshold_actual_order(void) { + const double exact = log(0.8 / 0.5); + const double tols[2] = {1e-4, 1e-9}; + for (int t = 0; t < 2; ++t) { + /* Below the escape energy -> DARK first, even at loose tolerance. */ + { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = 0.5; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(tols[t], 1.0, 10.0); + config.max_step = 1.0; + config.threshold = + (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = exact - 2.0e-5, + .policy_version = 3}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, + "order dark slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -3.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_DARK && + out.reason == RAY_REASON_REDSHIFT_LIMIT, + "sub-escape threshold is DARK"); + CHECK(out.threshold_value >= exact - 2.0e-5, + "DARK records a reached threshold"); + CHECK(out.stop_coordinate_time > -exact, + "DARK stop precedes the escape crossing"); + } + /* Above the escape energy -> ESCAPED. */ + { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = 0.5; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(tols[t], 1.0, 10.0); + config.max_step = 1.0; + config.threshold = + (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = exact + 1.0e-5, + .policy_version = 3}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -3.0, &slab) == 0, + "order escape slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -3.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "super-escape threshold escapes"); + } + } +} + +/* ------------------------------------------------------------------ */ +/* 13. Non-monotonic L pulse: a mid-trace dark truncation must fire */ +/* even when the accepted endpoint returns below the threshold. */ +/* */ +/* Time-dependent gamma metric: alpha = 1, beta = 0, gamma_ij = */ +/* a(t)^2 delta_ij, K_ij = -a(t)^2 H(t) delta_ij with a = exp(-F) and */ +/* H = d ln a/dt = F'(s), s = t0 - t. Spatial derivatives are zero. */ +/* With Pi_i = a n_i the past path has dL/ds = F'(s), so L(s) = F(s): */ +/* the metric's gamma time dependence and K are consistent (a partial */ +/* alpha(t) with K = 0 would not produce this evolution). */ +/* ------------------------------------------------------------------ */ + +typedef struct { + double A; + double t0; + double radius; + int kind; /* 0 = two-pulse 16 A s(1-s)(s-.5)^2, 1 = single 4 A s(1-s) */ + size_t end_count; +} CosmoContext; + +static double cosmo_amp(double s, double A, int kind) { + if (kind == 0) + return 16.0 * A * s * (1.0 - s) * (s - 0.5) * (s - 0.5); + if (kind == 2) + return A * sin(6.283185307179586476925286766559 * s); + return 4.0 * A * s * (1.0 - s); +} + +static double cosmo_amp_prime(double s, double A, int kind) { + if (kind == 0) + return 16.0 * A * (-4.0 * s * s * s + 6.0 * s * s - 2.5 * s + 0.25); + if (kind == 2) + return A * 6.283185307179586476925286766559 * + cos(6.283185307179586476925286766559 * s); + return 4.0 * A * (1.0 - 2.0 * s); +} + +static SpacetimePointStatus cosmo_eval(const SpacetimeSource *source, double t, + const double x[3], MetricData *metric) { + const CosmoContext *c = source->context; + (void)x; + const double s = c->t0 - t; + const double F = cosmo_amp(s, c->A, c->kind); + const double H = cosmo_amp_prime(s, c->A, c->kind); + const double a = exp(-F); + const double g = a * a; + *metric = flat_metric(); + metric->alpha = 1.0; + metric->gamma[0][0] = metric->gamma[1][1] = metric->gamma[2][2] = g; + metric->K[0][0] = metric->K[1][1] = metric->K[2][2] = -g * H; + return SPACETIME_POINT_OK; +} + +static size_t cosmo_end_count(const SpacetimeSource *source) { + return ((const CosmoContext *)source->context)->end_count; +} + +static int cosmo_end(const SpacetimeSource *source, size_t index, + SpacetimeAsymptoticEnd *out) { + const CosmoContext *c = source->context; + if (index >= c->end_count) + return -1; + *out = (SpacetimeAsymptoticEnd){ + .end_id = 0, + .exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI, + .mass = 0.0, + .frame_origin = {0.0, 0.0, 0.0}, + .frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + return 0; +} + +static int cosmo_worldtube(const SpacetimeSource *source, SpacetimeEndId end_id, + double t, SpacetimeEscapeWorldtubeSample *out) { + const CosmoContext *c = source->context; + (void)t; + if (end_id != 0) + return -1; + *out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0}, + .velocity = {0.0, 0.0, 0.0}, + .radius = c->radius, + .radius_rate = 0.0, + .velocity_constant = 1, + .valid = 1}; + return 0; +} + +static const SpacetimeOps cosmo_ops = { + .eval = cosmo_eval, + .classify = event_classify, + .asymptotic_end_count = cosmo_end_count, + .asymptotic_end = cosmo_end, + .escape_worldtube_sample = cosmo_worldtube, + .destroy = event_destroy, +}; + +/* Analytic first upcrossing of F = threshold for each pulse shape. */ +static double cosmo_first_crossing(double A, double threshold, int kind) { + if (kind == 0) { + const double disc = 0.0625 - threshold / (4.0 * A); + return 0.5 - sqrt((0.25 + sqrt(disc)) / 2.0); + } + return (1.0 - sqrt(1.0 - threshold / A)) / 2.0; +} + +static void cosmo_run(double A, double threshold, int kind, size_t end_count, + double tol, int expect_dark) { + CosmoContext c; + memset(&c, 0, sizeof c); + c.A = A; + c.t0 = 0.0; + c.radius = 100.0; + c.kind = kind; + c.end_count = end_count; + SpacetimeSource source = {.ops = &cosmo_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.Pi[0] = -1.0; /* null: gamma^{ij} Pi_i Pi_j = |Pi|^2/a^2 = 1 */ + state.log_alpha_p0 = 0.0; + state.log_alpha_p0_0 = 0.0; + GeodesicTraceConfig config = dp_config(tol, 1.0, 10.0); + config.max_step = 1.0; + config.max_steps = 100; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = threshold, + .policy_version = 1}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, "cosmo slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED, "cosmo terminates"); + if (!expect_dark) { + CHECK(out.outcome != RAY_OUTCOME_DARK, "cosmo expected non-dark"); + return; + } + CHECK(out.outcome == RAY_OUTCOME_DARK && + out.reason == RAY_REASON_REDSHIFT_LIMIT, + "mid-pulse dark truncation fires"); + CHECK(out.threshold_value >= threshold, + "dark records a reached threshold"); + const double s_ref = cosmo_first_crossing(A, threshold, kind); + CHECK(fabs(-out.stop_coordinate_time - s_ref) < 1e-5, + "dark stop is the first upcrossing"); + CHECK(-out.stop_coordinate_time < 0.1464466, + "dark stop is before the first peak"); +} + +static void test_event_nonmonotonic_threshold(void) { + const double A = 0.08; + const double T = 0.01; + /* Both accepted-step tolerances must find the same first crossing even + * though the accepted endpoint returns to L = 0 < T. */ + cosmo_run(A, T, 0, 1, 1e-3, 1); + cosmo_run(A, T, 0, 1, 1e-9, 1); + /* The same energy policy applies to a backend that declares no ends. */ + cosmo_run(A, T, 0, 0, 1e-3, 1); + /* A single non-monotonic pulse (one upcrossing) behaves the same way. */ + cosmo_run(A, T, 1, 1, 1e-3, 1); + cosmo_run(A, T, 1, 1, 1e-9, 1); +} + +/* Large-origin moving worldtube: the production dense polynomial must match + * the actual F at the same theta, so the event timing cannot drift with the + * rounding of the sampled half-step. */ +static void test_event_worldtube_polynomial_time_origin(void) { + const double t0 = 1.0e14; + EventContext c; + memset(&c, 0, sizeof c); + c.end_count = 1; + c.id[0] = 0; + c.center[0][0] = 0.0; + c.velocity[0][0] = -0.99; + c.t_ref[0] = t0; + c.radius_a[0] = 1.0; + c.velocity_constant[0] = 1; + SpacetimeSource source = {.ops = &event_ops, .context = &c}; + GeodesicTraceConfig config = dp_config(1e-9, 0.05, 1.0e6); + config.max_step = 0.05; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, t0, t0 - 1.0, &slab) == 0, "probe slab"); + /* Only theta values whose target t_before + h*theta is exactly representable + * compare cleanly; 1/3 lands one ULP inside the step for h = 3 ULP. */ + const double thetas[3] = {0.0, 1.0 / 3.0, 1.0}; + for (int k = 0; k < 3; ++k) { + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.coordinate_time = t0; + state.integration_start_time = t0; + state.x[0] = 0.5; + state.Pi[0] = -1.0; + double dense_value = NAN, actual_value = NAN; + CHECK(geodesic_event_worldtube_dense_probe(slab, &config, 0, &state, + t0 - 1.0, thetas[k], + &dense_value, &actual_value) == 1, + "worldtube dense probe runs"); + const double scale = fmax(1.0, fabs(actual_value)); + CHECK(fabs(dense_value - actual_value) < 1e-10 * scale, + "dense worldtube F matches actual F at a large time origin"); + } + spacetime_free_slab(slab); +} + +/* Explicitly prove the loose-tolerance pulse step is accepted as one step + * with both endpoints below the threshold (so the test covers the miss). */ +static void test_event_nonmonotonic_accepts_one_step(void) { + const double A = 0.08; + const double T = 0.01; + CosmoContext c; + memset(&c, 0, sizeof c); + c.A = A; + c.radius = 100.0; + c.kind = 0; + c.end_count = 1; + SpacetimeSource source = {.ops = &cosmo_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-3, 1.0, 10.0); + config.max_step = 1.0; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = T, + .policy_version = 1}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, + "cosmo one-step slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_DARK, + "loose-tolerance pulse is dark"); + CHECK(out.accepted_steps == 1u, + "loose tolerance accepts the whole pulse step in one step"); + /* Both endpoints of the accepted step are genuinely below the threshold, so + * the dark stop can only come from the mid-step pulse. */ + CHECK(cosmo_amp(0.0, A, 0) < T && cosmo_amp(1.0, A, 0) < T, + "pulse endpoints are below the threshold"); + CHECK(out.threshold_value >= T, "pulse dark reached the threshold"); +} + +/* The threshold is a closed (>=) event: a crossing exactly at the step/slab + * endpoint must be accepted through the closed-end candidate, not sent into a + * permanent shrink. */ +static void test_event_threshold_closed_endpoint(void) { + AlphaContext c; + memset(&c, 0, sizeof c); + c.k = 1.0; + c.radius = 10.0; + SpacetimeSource source = {.ops = &alpha_ops, .context = &c}; + + /* Phase 1: with the threshold disabled, find the exactly accepted endpoint + * monitored value at s = 1 (L - L0 = s along this ray). */ + GeodesicRayState probe; + alpha_state(0.2, c.k, &probe); + GeodesicTraceConfig pconfig = dp_config(1e-4, 1.0, 10.0); + pconfig.max_step = 1.0; + pconfig.threshold.kind = THRESHOLD_DISABLED; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, + "closed endpoint slab"); + RayEndpoint out = blank_endpoint(); + (void)geodesic_advance_past_ray(slab, &probe, -1.0, &pconfig, &out); + const double endpoint_value = probe.log_alpha_p0 - probe.log_alpha_p0_0; + CHECK(fabs(probe.coordinate_time + 1.0) < 1e-9, + "endpoint probe reached the slab boundary"); + CHECK(fabs(endpoint_value - 1.0) < 1e-9, "endpoint value is near one"); + + /* Phase 2: threshold set to that endpoint value: a closed (>=) event at the + * step/slab endpoint must be a DARK event, not a permanent shrink. */ + GeodesicRayState state; + alpha_state(0.2, c.k, &state); + GeodesicTraceConfig config = dp_config(1e-4, 1.0, 10.0); + config.max_step = 1.0; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = endpoint_value, + .policy_version = 1}; + (void)geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(out.outcome == RAY_OUTCOME_DARK && + out.reason == RAY_REASON_REDSHIFT_LIMIT, + "endpoint threshold is a dark event"); + CHECK(fabs(out.stop_coordinate_time + 1.0) < 1e-6, + "endpoint threshold is localized at the endpoint"); + CHECK(out.threshold_value >= endpoint_value, "endpoint threshold reached"); +} + +/* When an event candidate cannot be confirmed on the actual trajectory and + * the bounded shrink is exhausted, this is an INCOMPLETE integration failure, + * never a rewritten DARK with a restored (steps = 0) state. */ +static void test_event_threshold_confirmation_exhausted(void) { + CosmoContext c; + memset(&c, 0, sizeof c); + c.A = 0.1; + c.radius = 100.0; + c.kind = 2; /* non-polynomial L pulse; the coarse dense quartic cannot be + confirmed at this step/tolerance */ + c.end_count = 1; + SpacetimeSource source = {.ops = &cosmo_ops, .context = &c}; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0, + "confirmation slab"); + + /* Phase 1: with the threshold disabled the same coarse step is accepted, so + * the phase-2 failure is an event-confirmation failure, not a step rejection. */ + GeodesicRayState probe; + memset(&probe, 0, sizeof probe); + probe.Pi[0] = -1.0; + GeodesicTraceConfig pconfig = dp_config(1.0, 1.0, 10.0); + pconfig.max_step = 1.0; + pconfig.max_steps = 100; + pconfig.consecutive_rejection_limit = 1; + pconfig.atol_L = 1.0e6; + pconfig.rtol = 1.0e6; + pconfig.threshold.kind = THRESHOLD_DISABLED; + RayEndpoint probe_out = blank_endpoint(); + (void)geodesic_advance_past_ray(slab, &probe, -1.0, &pconfig, &probe_out); + CHECK(probe.steps >= 1u, "the coarse step itself is accepted"); + + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1.0, 1.0, 10.0); + config.max_step = 1.0; + config.max_steps = 100; + config.consecutive_rejection_limit = 1; + config.atol_L = 1.0e6; + config.rtol = 1.0e6; + config.threshold = (ThresholdPolicy){.kind = THRESHOLD_LOG_ENERGY_GROWTH, + .value = 0.13, + .policy_version = 1}; + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -1.0, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_FAILED && + out.outcome == RAY_OUTCOME_INCOMPLETE && + out.reason == RAY_REASON_INTEGRATION_ERROR, + "unconfirmable threshold is an incomplete integration failure"); + CHECK(out.outcome != RAY_OUTCOME_DARK, + "unconfirmable threshold is not rewritten into DARK"); + CHECK(state.steps == 0u, + "last trusted state carries no fabricated accepted step"); + CHECK(state.rejected_steps >= 1u, "rejection cost is real"); + CHECK(out.rhs_evaluations > 0ul, "actual RHS cost is recorded"); +} + +/* Uniform unit scaling: with every length/time quantity scaled by 1e-9 the + * same flat escape must give event time/scale ~ 3 and x/scale ~ 5, i.e. no + * absolute coordinate-time floor breaks small units. */ +static void test_event_unit_scaling(void) { + const double scale = 1.0e-9; + FixtureContext c; + memset(&c, 0, sizeof c); + c.radius = 5.0 * scale; + SpacetimeSource source = {.ops = &fixture_ops, .context = &c}; + GeodesicRayState state; + memset(&state, 0, sizeof state); + state.x[0] = 2.0 * scale; + state.Pi[0] = -1.0; + GeodesicTraceConfig config = dp_config(1e-10, 0.5, 100.0); + config.coordinate_time_step *= scale; + config.min_step *= scale; + config.max_step *= scale; + config.atol_x *= scale; + config.max_lookback_time *= scale; + MetricSlab *slab = NULL; + CHECK(spacetime_load_slab(&source, 0.0, -20.0 * scale, &slab) == 0, + "scaled slab"); + RayEndpoint out = blank_endpoint(); + const GeodesicAdvanceResult r = + geodesic_advance_past_ray(slab, &state, -20.0 * scale, &config, &out); + spacetime_free_slab(slab); + CHECK(r == GEODESIC_ADVANCE_TERMINATED && + out.outcome == RAY_OUTCOME_ESCAPED, + "scaled flat escape"); + CHECK(fabs((0.0 - out.stop_coordinate_time) / scale - 3.0) < 1e-6, + "scaled event time / scale is 3"); + CHECK(fabs(out.final_x[0] / scale - 5.0) < 1e-6, + "scaled crossing position / scale is 5"); +} + +int main(void) { + test_minkowski_finite_interval(); + test_schwarzschild_radial(); + test_over_large_initial_step(); + test_fixture_stage_retry(); + test_fixture_fatal_and_bounds(); + test_budget_lookback_and_resume(); + test_flat_escape_localization(); + test_event_time_translation(); + test_event_subintegration_time_hole(); + test_schwarzschild_dp_escape_matches_finish(); + test_rk4_compatibility(); + test_ray_pool_batch_matches_single(); + test_ray_pool_continuation_state(); + test_ray_pool_cross_slab_reject(); + test_event_polynomial_roots(); + test_event_two_ends_order(); + test_event_segmented_worldtube(); + test_event_nonconstant_unsupported(); + test_event_threshold_order(); + test_event_rejected_stage_threshold(); + test_rk4_rhs_cost(); + test_unknown_stepper_rejected(); + test_event_curved_in_out_in(); + test_event_curved_tangent(); + test_event_curved_endpoint_root(); + test_event_log_p0_accepted_state(); + test_event_step_time_precision(); + test_event_absorbed_endpoint_exit(); + test_event_boundary_roundoff_exit(); + test_event_clearly_outside_protocol(); + test_event_threshold_actual_order(); + test_event_nonmonotonic_threshold(); + test_event_nonmonotonic_accepts_one_step(); + test_event_worldtube_polynomial_time_origin(); + test_event_threshold_closed_endpoint(); + test_event_threshold_confirmation_exhausted(); + test_event_unit_scaling(); + if (failures != 0) { + fprintf(stderr, "geodesic adaptive regression: %d failure(s)\n", failures); + return 1; + } + puts("geodesic adaptive regression passed"); + return 0; +} diff --git a/usage.md b/usage.md index 77bec4e..a6d5fff 100644 --- a/usage.md +++ b/usage.md @@ -208,6 +208,59 @@ Movie rays from all frames share a newest-to-oldest coordinate-time sweep. backends use logical slabs without metric I/O; [Nmesh](https://github.com/nmeshsource/nmesh) metric loading remains future work. +## Geodesic integration and tracing budgets + +The default `--integrator dp54` selects adaptive Dormand–Prince 5(4). Position, photon +direction/momentum, and log-energy errors have separate absolute tolerances: + +```text +--ode-rtol R +--ode-atol-x X +--ode-atol-pi P +--ode-atol-l L +--ode-initial-step H +--ode-min-step HMIN +--ode-max-step HMAX +--ode-max-rejections N +``` + +All tolerance and step values must be finite and positive; the initial step +must lie between the step bounds. The position absolute tolerance has the +backend's coordinate-length units. Smaller tolerances control local ODE error, +not a guaranteed bound on final sky-direction or image error near critical rays. + +Default minimum steps are `1e-12` in Minkowski and Schwarzschild (`M=1`), and +`1e-12 * R` in Alcubierre. This is a conservative numerical guard, not a measured +physical minimum. Default maximum steps are `16`, `8M`, and eight times +`min(0.1, 0.05/sigma)`, respectively. Schwarzschild starts at `0.1M`; increasing +the upper bound does not force large steps through strong-field regions. +Use tolerance convergence for near-critical rays rather than interpreting the +upper bound as a global accuracy guarantee. See the bounded +[step-bound experiments](benchmarks/adaptive_step_bounds_2026-10-05/README.md). + +Tracing has independent accepted-step and coordinate-time budgets: + +```text +--trace-max-steps N +--trace-lookback-time T +--retry-step-increment N +--max-total-steps N +--retry-lookback-increment T +--max-total-lookback-time T +``` + +Changing the accepted-step allowance does not change the time allowance. A +trustworthy ray that exhausts either allowance is unresolved and can be resumed +by refinement with additional resources; it is not a physical dark endpoint. +Retries keep the last accepted state and camera energy reference, without +relaxing numerical tolerances. A hard limit that prevents a required retry +blocks normal output; `--allow-incomplete` is a diagnostic override. + +`--integrator rk4` retains the fixed-step comparison path. Adaptive-only +tolerances and time-budget overrides are not applicable to that path. Use an +explicit stepper when reproducing a fixed-step reference rather than relying +on the executable's default. + ## Reuse a completed lens map `--lens-map-output FILE` saves the finalized inverse-lens mesh after tracing @@ -237,6 +290,13 @@ on import. Explicit conflicting dimensions or FOV are rejected. The reader checks the format version, finite values, unit directions, triangle indices, and per-frame CRCs. +The map also records the stepper, numerical tolerances and step bounds, tracing +and retry allowances, and accepted/rejected/RHS costs. Replay uses this saved +provenance; tracing-option overrides are rejected because replay does not +integrate rays. Version 3 preserves adaptive provenance. Version 2 imports as +fixed RK4 with unavailable adaptive fields and cost diagnostics; version 1 is +rejected because its capture semantics cannot be reconstructed reliably. + A movie export stores all final frame meshes in one file. To render it again, pass `--lens-map-input FILE`, the catalog, `--frames-dir DIR`, and `--frames-prefix NAME`; no observer track is needed on import.