Files
wyj 0a46a7095b Feat: Complete adaptive geodesic tracing with DP54
Add error-controlled DP5(4) integration and trusted first-crossing localization, including non-monotonic energy thresholds and representable-time stepping.

Preserve adaptive state and independent step/time retry grants across RayPool, refinement and movie scheduling. Expose numerical controls, record actual persistent-sample costs, and add v3 lens-map provenance with legacy v2 RK4 import.

Use DP54 by default and select an 8M Schwarzschild maximum step from bounded scans and a two-run 4K comparison. Retain the conservative minimum-step guard and document critical-ray and backend capability limits. Archive self-contained benchmark inputs and raw output; keep fixed RK4 HDR references explicit.

Validation: make -B -j4 BUILD_TYPE=Debug test passed; explicit RK4 HDR references have zero differences. Bounded convergence checks, benchmark reproduction, Release build and focused reviews passed. No numerical-relativity backend is added.
2026-10-05 20:27:42 -04:00

342 lines
12 KiB
C

/*
* 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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}