Fix: Distinguish concrete ray failure causes
This commit is contained in:
1 parent
76ed705d37
commit
95c05f84d9
12 files changed
+580
-111
No files matched your search
+52
-1
@@ -499,11 +499,61 @@ static void test_end_protocol_error(void) {
|
||||
CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) ==
|
||||
GEODESIC_ADVANCE_FAILED &&
|
||||
endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_PROTOCOL_ERROR,
|
||||
endpoint.reason == RAY_REASON_END_DESCRIPTOR_FAILED,
|
||||
"advance rejects a declared-but-broken end without legacy");
|
||||
spacetime_free_slab(slab);
|
||||
}
|
||||
|
||||
/* A worldtube sample callback failure and an invalid worldtube geometry are
|
||||
* distinguished from each other and from the lifecycle/descriptor failure
|
||||
* above when the same advance entry point classifies its pre-step scan. */
|
||||
static void test_advance_worldtube_failure_reasons(void) {
|
||||
const GeodesicTraceConfig config = {.coordinate_time_step = 1.0,
|
||||
.max_steps = 10};
|
||||
GeodesicRayState state = {.coordinate_time = 0.0,
|
||||
.x = {1.0, 0.0, 0.0},
|
||||
.Pi = {0.0, 0.0, 0.0},
|
||||
.log_alpha_p0 = 0.0,
|
||||
.steps = 0};
|
||||
|
||||
SyntheticContext sample_fail = {.radius = 20.0,
|
||||
.valid_t_min = -1.0e30,
|
||||
.constant = 1,
|
||||
.sample_callback_fails = 1};
|
||||
SpacetimeSource source = {.ops = &synthetic_ops, .context = &sample_fail};
|
||||
MetricSlab *slab = NULL;
|
||||
CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0,
|
||||
"sample-fail slab");
|
||||
RayEndpoint endpoint = {.frequency_ratio = 0.0,
|
||||
.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE};
|
||||
CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) ==
|
||||
GEODESIC_ADVANCE_FAILED &&
|
||||
endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_WORLDTUBE_SAMPLE_FAILED,
|
||||
"worldtube sample callback failure is its own reason");
|
||||
spacetime_free_slab(slab);
|
||||
|
||||
SyntheticContext bad_geometry = {.radius = 20.0,
|
||||
.valid_t_min = -1.0e30,
|
||||
.constant = 1,
|
||||
.sample_nan_radius = 1};
|
||||
source = (SpacetimeSource){.ops = &synthetic_ops, .context = &bad_geometry};
|
||||
CHECK(spacetime_load_slab(&source, 0.0, -10.0, &slab) == 0,
|
||||
"geometry slab");
|
||||
endpoint = (RayEndpoint){.frequency_ratio = 0.0,
|
||||
.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE};
|
||||
CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) ==
|
||||
GEODESIC_ADVANCE_FAILED &&
|
||||
endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_WORLDTUBE_GEOMETRY_INVALID,
|
||||
"invalid worldtube geometry is its own reason");
|
||||
spacetime_free_slab(slab);
|
||||
}
|
||||
|
||||
static void test_interior_crossing_bisection_failure(void) {
|
||||
/* radius 20.3 makes the exit land strictly between steps: the accepted
|
||||
* step goes from F < 0 (t = -119.7) to F > 0 (t = -120.7). The invalid
|
||||
@@ -753,6 +803,7 @@ int main(void) {
|
||||
test_motion_segment_domain();
|
||||
test_schwarzschild_sample_failures();
|
||||
test_end_protocol_error();
|
||||
test_advance_worldtube_failure_reasons();
|
||||
test_nonconstant_preroute_unsupported();
|
||||
test_piecewise_constant_history_hole();
|
||||
test_interior_history_exhaustion();
|
||||
|
||||
@@ -1418,6 +1418,10 @@ int main(void) {
|
||||
.outcome = RAY_OUTCOME_DARK,
|
||||
.reason = RAY_REASON_REDSHIFT_LIMIT,
|
||||
.end_id = SPACETIME_END_NONE, .traced = 1};
|
||||
/* An appended detail reason with its coherent outcome must survive the
|
||||
* frozen v3 schema exactly. */
|
||||
dv[2].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
dv[2].reason = RAY_REASON_REJECTION_LIMIT;
|
||||
dv[0].trace_accepted_steps = 11; dv[0].trace_rejected_steps = 2;
|
||||
dv[0].trace_rhs_evaluations = 79;
|
||||
dv[1].trace_accepted_steps = 5;
|
||||
@@ -1458,6 +1462,8 @@ int main(void) {
|
||||
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.vertices[2].outcome != RAY_OUTCOME_INCOMPLETE ||
|
||||
dloaded.frames[0].mesh.vertices[2].reason != RAY_REASON_REJECTION_LIMIT ||
|
||||
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;
|
||||
@@ -1497,6 +1503,41 @@ int main(void) {
|
||||
lens_map_destroy(&dloaded);
|
||||
}
|
||||
}
|
||||
/* RAY_REASON_COUNT is a sentinel, never a valid wire reason: a map that
|
||||
* stores it (or anything above it) must be rejected rather than
|
||||
* reinterpreted. Vertex 0's reason field starts at byte 304 in this v3
|
||||
* layout (176 provenance + 48 frame header + 80 vertex prefix). */
|
||||
{
|
||||
if (lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1)) {
|
||||
fputs("lens-map sentinel-reason fixture write failed\n", stderr);
|
||||
unlink(dp_path); goto done;
|
||||
}
|
||||
const uint32_t sentinel = (uint32_t)RAY_REASON_COUNT;
|
||||
FILE *bad = fopen(dp_path, "r+b");
|
||||
int bad_failed = bad == NULL || fseek(bad, 304, SEEK_SET) ||
|
||||
fwrite(&sentinel, sizeof sentinel, 1, bad) != 1;
|
||||
if (bad != NULL && fclose(bad)) bad_failed = 1;
|
||||
if (bad_failed || !lens_map_read(dp_path, NULL, &dloaded) ||
|
||||
dloaded.frames != NULL || dloaded.frame_count != 0) {
|
||||
fputs("lens-map sentinel reason rejection regression failed\n", stderr);
|
||||
lens_map_destroy(&dloaded); unlink(dp_path); goto done;
|
||||
}
|
||||
lens_map_destroy(&dloaded);
|
||||
}
|
||||
/* The in-memory writer must also reject the sentinel; the wire test above
|
||||
* cannot isolate reason validation from the frame CRC. */
|
||||
{
|
||||
const RayReason saved_reason = dv[2].reason;
|
||||
dv[2].reason = (RayReason)RAY_REASON_COUNT;
|
||||
const int rejected =
|
||||
lens_map_write(dp_path, 4, 3, 30.0, &dp, &df, 1) != 0;
|
||||
dv[2].reason = saved_reason;
|
||||
if (!rejected) {
|
||||
fputs("lens-map in-memory sentinel reason write regression failed\n",
|
||||
stderr);
|
||||
unlink(dp_path); goto done;
|
||||
}
|
||||
}
|
||||
unlink(dp_path);
|
||||
}
|
||||
/* Legacy v2 import: a real v2 map (no adaptive fields, no cost counters)
|
||||
|
||||
+94
-17
@@ -7,26 +7,103 @@
|
||||
|
||||
static int nearly_equal(double a, double b) { return fabs(a - b) < 1e-12; }
|
||||
|
||||
/* Every enumerator must have a stable label, and an out-of-range value must be
|
||||
* reported as UNKNOWN rather than indexing past a table. */
|
||||
/* Every enumerator must have a stable label, appended detail codes must be
|
||||
* distinct and valid, the coarse category helper must stay inside the coarse
|
||||
* range, and both out-of-range directions (negative and >= COUNT) must be
|
||||
* rejected as UNKNOWN/sentinel. The frozen wire ids 0..9 are pinned. */
|
||||
static int check_reason_names(void) {
|
||||
const RayReason reasons[] = {
|
||||
RAY_REASON_NONE, RAY_REASON_REDSHIFT_LIMIT,
|
||||
RAY_REASON_BUDGET_EXHAUSTED, RAY_REASON_TIME_RANGE_EXHAUSTED,
|
||||
RAY_REASON_OUT_OF_DOMAIN, RAY_REASON_INVALID_METRIC,
|
||||
RAY_REASON_INTEGRATION_ERROR, RAY_REASON_UNSUPPORTED,
|
||||
RAY_REASON_PROTOCOL_ERROR, RAY_REASON_IO_ERROR};
|
||||
static const char *const frozen[] = {
|
||||
"NONE", "REDSHIFT_LIMIT", "BUDGET_EXHAUSTED",
|
||||
"TIME_RANGE_EXHAUSTED", "OUT_OF_DOMAIN", "INVALID_METRIC",
|
||||
"INTEGRATION_ERROR", "UNSUPPORTED", "PROTOCOL_ERROR",
|
||||
"IO_ERROR"};
|
||||
int failed = 0;
|
||||
for (size_t i = 0; i < sizeof reasons / sizeof reasons[0]; ++i) {
|
||||
const char *name = ray_reason_name(reasons[i]);
|
||||
if (name == NULL || name[0] == '\0' || strcmp(name, "UNKNOWN") == 0) {
|
||||
fprintf(stderr, "reason %d has no stable label\n", (int)reasons[i]);
|
||||
for (size_t i = 0; i < sizeof frozen / sizeof frozen[0]; ++i) {
|
||||
const RayReason r = (RayReason)i;
|
||||
if (!ray_reason_valid(r) || strcmp(ray_reason_name(r), frozen[i]) != 0 ||
|
||||
ray_reason_category(r) != r) {
|
||||
fprintf(stderr, "frozen reason id %zu is not stable\n", i);
|
||||
failed = 1;
|
||||
}
|
||||
}
|
||||
const int unknown = (int)RAY_REASON_IO_ERROR + 7;
|
||||
if (strcmp(ray_reason_name((RayReason)unknown), "UNKNOWN") != 0) {
|
||||
fputs("out-of-range reason is not UNKNOWN\n", stderr);
|
||||
if ((unsigned)RAY_REASON_IO_ERROR + 1u >= (unsigned)RAY_REASON_COUNT) {
|
||||
fputs("no appended detail reasons\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
for (unsigned i = 0; i < (unsigned)RAY_REASON_COUNT; ++i) {
|
||||
const RayReason r = (RayReason)i;
|
||||
const char *name = ray_reason_name(r);
|
||||
if (!ray_reason_valid(r) || name == NULL || name[0] == '\0' ||
|
||||
strcmp(name, "UNKNOWN") == 0) {
|
||||
fprintf(stderr, "reason %u has no stable label\n", i);
|
||||
failed = 1;
|
||||
}
|
||||
if (!ray_reason_valid(ray_reason_category(r))) {
|
||||
fprintf(stderr, "reason %u has no valid category\n", i);
|
||||
failed = 1;
|
||||
}
|
||||
}
|
||||
if (ray_reason_category(RAY_REASON_WORLDTUBE_SAMPLE_FAILED) !=
|
||||
RAY_REASON_PROTOCOL_ERROR ||
|
||||
ray_reason_category(RAY_REASON_OUTSIDE_WORLDTUBE) !=
|
||||
RAY_REASON_PROTOCOL_ERROR ||
|
||||
ray_reason_category(RAY_REASON_ESCAPE_LOCALIZATION_FAILED) !=
|
||||
RAY_REASON_PROTOCOL_ERROR ||
|
||||
ray_reason_category(RAY_REASON_REJECTION_LIMIT) !=
|
||||
RAY_REASON_INTEGRATION_ERROR ||
|
||||
ray_reason_category(RAY_REASON_INVALID_ESCAPE_DIRECTION) !=
|
||||
RAY_REASON_INTEGRATION_ERROR ||
|
||||
ray_reason_category(RAY_REASON_SLAB_LOAD_FAILED) != RAY_REASON_IO_ERROR) {
|
||||
fputs("detail reasons map to the wrong coarse category\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
if (ray_reason_valid((RayReason)RAY_REASON_COUNT) ||
|
||||
ray_reason_valid((RayReason)-1)) {
|
||||
fputs("out-of-range reason accepted as valid\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
if (strcmp(ray_reason_name((RayReason)RAY_REASON_COUNT), "UNKNOWN") != 0) {
|
||||
fputs("sentinel reason name is not UNKNOWN\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
const RayReason unknown = (RayReason)((unsigned)RAY_REASON_COUNT + 7u);
|
||||
if (strcmp(ray_reason_name(unknown), "UNKNOWN") != 0 ||
|
||||
ray_reason_valid(unknown) ||
|
||||
ray_reason_category(unknown) != (RayReason)RAY_REASON_COUNT) {
|
||||
fputs("unknown reason is not the UNKNOWN sentinel\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
/* A NULL, non-finite or non-unit direction must be rejected as INVALID_ARGUMENT
|
||||
* without dereferencing the direction or running any trace. */
|
||||
static int check_invalid_directions(const SpacetimeSource *source,
|
||||
const ObserverState *observer) {
|
||||
const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
|
||||
.max_steps = 100};
|
||||
int failed = 0;
|
||||
const RayEndpoint null_dir =
|
||||
geodesic_trace_past(source, observer, NULL, &config);
|
||||
if (null_dir.outcome != RAY_OUTCOME_INCOMPLETE ||
|
||||
null_dir.reason != RAY_REASON_INVALID_ARGUMENT) {
|
||||
fputs("NULL direction is not INVALID_ARGUMENT\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
const double nan_dir[3] = {NAN, 0.0, 0.0};
|
||||
const RayEndpoint nan =
|
||||
geodesic_trace_past(source, observer, nan_dir, &config);
|
||||
if (nan.outcome != RAY_OUTCOME_INCOMPLETE ||
|
||||
nan.reason != RAY_REASON_INVALID_ARGUMENT) {
|
||||
fputs("NaN direction is not INVALID_ARGUMENT\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
const double nonunit_dir[3] = {2.0, 0.0, 0.0};
|
||||
const RayEndpoint nonunit =
|
||||
geodesic_trace_past(source, observer, nonunit_dir, &config);
|
||||
if (nonunit.outcome != RAY_OUTCOME_INCOMPLETE ||
|
||||
nonunit.reason != RAY_REASON_INVALID_ARGUMENT) {
|
||||
fputs("non-unit direction is not INVALID_ARGUMENT\n", stderr);
|
||||
failed = 1;
|
||||
}
|
||||
return failed;
|
||||
@@ -35,8 +112,7 @@ static int check_reason_names(void) {
|
||||
static int check_ray(const SpacetimeSource *source,
|
||||
const ObserverState *observer,
|
||||
const double local_direction[3],
|
||||
const double expected[3]) {
|
||||
const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
|
||||
const double expected[3]) { const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
|
||||
.max_steps = 100};
|
||||
RayEndpoint ray =
|
||||
geodesic_trace_past(source, observer, local_direction, &config);
|
||||
@@ -63,6 +139,7 @@ int main(void) {
|
||||
!spacetime_slab_eval(slab, 0.25, (double[]){0.0, 0.0, 0.0}, &metric))
|
||||
return 1;
|
||||
int result = check_reason_names() ||
|
||||
check_invalid_directions(&source, &observer) ||
|
||||
check_ray(&source, &observer, (double[]){1.0, 0.0, 0.0},
|
||||
(double[]){0.0, 0.0, -1.0}) ||
|
||||
check_ray(&source, &observer, (double[]){0.0, 0.0, 1.0},
|
||||
|
||||
@@ -345,26 +345,26 @@ static void test_minkowski_finite_interval(void) {
|
||||
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,
|
||||
out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
|
||||
"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,
|
||||
out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
|
||||
"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,
|
||||
out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
|
||||
"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,
|
||||
out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
|
||||
"zero reject limit rejected");
|
||||
spacetime_free_slab(s);
|
||||
}
|
||||
@@ -669,7 +669,7 @@ static void test_fixture_fatal_and_bounds(void) {
|
||||
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,
|
||||
out.reason == RAY_REASON_METRIC_INTERNAL_ERROR,
|
||||
"stage INTERNAL_ERROR is a direct protocol failure");
|
||||
CHECK(state.rhs_evaluations == 2u, "internal error not retried");
|
||||
}
|
||||
@@ -694,7 +694,7 @@ static void test_fixture_fatal_and_bounds(void) {
|
||||
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,
|
||||
out.reason == RAY_REASON_MIN_STEP_REACHED,
|
||||
"step below hmin is an integration error");
|
||||
CHECK(state.rejected_steps >= 1u, "hmin path rejected before failing");
|
||||
|
||||
@@ -1786,7 +1786,7 @@ static void test_unknown_stepper_rejected(void) {
|
||||
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,
|
||||
traced.reason == RAY_REASON_UNKNOWN_STEPPER,
|
||||
"trace_past rejects an unknown stepper");
|
||||
|
||||
GeodesicRayState state;
|
||||
@@ -1794,7 +1794,7 @@ static void test_unknown_stepper_rejected(void) {
|
||||
const RayEndpoint resumed =
|
||||
geodesic_trace_past_from_state(&source, &state, &config);
|
||||
CHECK(resumed.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
resumed.reason == RAY_REASON_PROTOCOL_ERROR,
|
||||
resumed.reason == RAY_REASON_UNKNOWN_STEPPER,
|
||||
"trace_past_from_state rejects an unknown stepper");
|
||||
|
||||
MetricSlab *slab = NULL;
|
||||
@@ -1805,7 +1805,7 @@ static void test_unknown_stepper_rejected(void) {
|
||||
spacetime_free_slab(slab);
|
||||
CHECK(r == GEODESIC_ADVANCE_FAILED &&
|
||||
advanced.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
advanced.reason == RAY_REASON_PROTOCOL_ERROR,
|
||||
advanced.reason == RAY_REASON_UNKNOWN_STEPPER,
|
||||
"advance rejects an unknown stepper");
|
||||
spacetime_destroy(&source);
|
||||
}
|
||||
@@ -2041,6 +2041,35 @@ static void test_event_step_time_precision(void) {
|
||||
fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12,
|
||||
"time-precision fixture stays null");
|
||||
}
|
||||
|
||||
/* At a huge coordinate-time origin a nominal step below the local ULP
|
||||
* cannot define a representable nonzero interval: report the exact
|
||||
* TIME_STEP_UNREPRESENTABLE cause instead of committing a zero-length step. */
|
||||
{
|
||||
const double origin = 1.0e17;
|
||||
FixtureContext c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.radius = 1.0e30;
|
||||
SpacetimeSource source = {.ops = &fixture_ops, .context = &c};
|
||||
GeodesicRayState state;
|
||||
memset(&state, 0, sizeof state);
|
||||
state.coordinate_time = origin;
|
||||
state.integration_start_time = origin;
|
||||
state.Pi[0] = -1.0;
|
||||
GeodesicTraceConfig config = dp_config(1e-9, 1e-12, 1.0e30);
|
||||
config.min_step = 1e-12;
|
||||
config.max_step = 1.0;
|
||||
MetricSlab *slab = NULL;
|
||||
CHECK(spacetime_load_slab(&source, origin, origin - 1.0e30, &slab) == 0,
|
||||
"unrepresentable slab");
|
||||
RayEndpoint out = blank_endpoint();
|
||||
const GeodesicAdvanceResult r =
|
||||
geodesic_advance_past_ray(slab, &state, origin - 1.0e30, &config, &out);
|
||||
spacetime_free_slab(slab);
|
||||
CHECK(r == GEODESIC_ADVANCE_FAILED &&
|
||||
out.reason == RAY_REASON_TIME_STEP_UNREPRESENTABLE,
|
||||
"sub-ULP step is unrepresentable");
|
||||
}
|
||||
}
|
||||
|
||||
/* A crossing whose dense root is absorbed at theta == 1 (F_after strictly
|
||||
@@ -2099,8 +2128,10 @@ static void test_event_boundary_roundoff_exit(void) {
|
||||
"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. */
|
||||
/* A single-end state clearly outside its worldtube is reported with the exact
|
||||
* OUTSIDE_WORLDTUBE cause, not an unbounded search that can only end as budget
|
||||
* exhaustion, and the last trusted accepted state plus the real cost counters
|
||||
* are preserved (the failed event step is never committed). */
|
||||
static void test_event_clearly_outside_protocol(void) {
|
||||
FixtureContext c;
|
||||
memset(&c, 0, sizeof c);
|
||||
@@ -2121,8 +2152,15 @@ static void test_event_clearly_outside_protocol(void) {
|
||||
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");
|
||||
out.reason == RAY_REASON_OUTSIDE_WORLDTUBE,
|
||||
"clearly-outside single end is an explicit failure");
|
||||
CHECK(out.stop_coordinate_time == 0.0 && out.final_x[0] == 2.0 &&
|
||||
out.final_x[1] == 0.0 && out.final_x[2] == 0.0 &&
|
||||
out.accepted_steps == 0u,
|
||||
"outside-worldtube failure preserves the last trusted state");
|
||||
CHECK(state.coordinate_time == 0.0 && state.x[0] == 2.0 &&
|
||||
state.steps == 0u && state.rhs_evaluations > 0ul,
|
||||
"outside-worldtube failure keeps the trusted state and real cost");
|
||||
}
|
||||
|
||||
/* Escape and threshold are both localized on the actual trajectory and ordered
|
||||
@@ -2529,7 +2567,7 @@ static void test_event_threshold_confirmation_exhausted(void) {
|
||||
spacetime_free_slab(slab);
|
||||
CHECK(r == GEODESIC_ADVANCE_FAILED &&
|
||||
out.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
out.reason == RAY_REASON_INTEGRATION_ERROR,
|
||||
out.reason == RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED,
|
||||
"unconfirmable threshold is an incomplete integration failure");
|
||||
CHECK(out.outcome != RAY_OUTCOME_DARK,
|
||||
"unconfirmable threshold is not rewritten into DARK");
|
||||
@@ -2539,6 +2577,35 @@ static void test_event_threshold_confirmation_exhausted(void) {
|
||||
CHECK(out.rhs_evaluations > 0ul, "actual RHS cost is recorded");
|
||||
}
|
||||
|
||||
/* A pure error-estimate rejection that exhausts the consecutive-rejection quota
|
||||
* reports the exact REJECTION_LIMIT cause. With a one-rejection quota the
|
||||
* limit is reached before any minimum-step check, so this is not MIN_STEP. */
|
||||
static void test_rejection_limit_reason(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-15, 1.0, 10.0);
|
||||
config.max_step = 1.0;
|
||||
config.min_step = 1.0;
|
||||
config.consecutive_rejection_limit = 1;
|
||||
MetricSlab *slab = NULL;
|
||||
CHECK(spacetime_load_slab(&source, 0.0, -5.0, &slab) == 0,
|
||||
"reject-limit 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.reason == RAY_REASON_REJECTION_LIMIT,
|
||||
"error-estimate rejection quota reports REJECTION_LIMIT");
|
||||
CHECK(state.coordinate_time == 0.0 && state.rejected_steps >= 1u,
|
||||
"rejected trial did not advance the accepted state");
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
@@ -2611,6 +2678,7 @@ int main(void) {
|
||||
test_event_worldtube_polynomial_time_origin();
|
||||
test_event_threshold_closed_endpoint();
|
||||
test_event_threshold_confirmation_exhausted();
|
||||
test_rejection_limit_reason();
|
||||
test_event_unit_scaling();
|
||||
if (failures != 0) {
|
||||
fprintf(stderr, "geodesic adaptive regression: %d failure(s)\n", failures);
|
||||
|
||||
@@ -11,8 +11,9 @@ refusal must not recommend a larger retry budget for a pure integration error.
|
||||
|
||||
A deterministic DP54 integration error is produced without a large workload:
|
||||
strict tolerances, ``min == initial == max`` step, and one allowed rejection
|
||||
make the first trial step fail as ``INTEGRATION_ERROR``. Small 16x8 scenes
|
||||
keep every case short. Two-frame movie coverage uses the existing
|
||||
make the first trial step fail. The rejection quota is reached before any
|
||||
minimum-step check, so the exact detailed reason is ``REJECTION_LIMIT``. Small
|
||||
16x8 scenes keep every case short. Two-frame movie coverage uses the existing
|
||||
``test_observer_<backend>`` fixture track duplicated to two rows (the
|
||||
Schwarzschild metric is stationary, so the same tetrad is valid at both
|
||||
times); the test then checks that each failing sample is counted once across
|
||||
@@ -48,9 +49,10 @@ 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]
|
||||
|
||||
# Deterministic DP54 integration failure: tight tolerances, a single fixed step
|
||||
# bound, and one allowed rejection. Every ray rejects its first trial step and
|
||||
# reports INTEGRATION_ERROR instead of a fabricated terminal category.
|
||||
# Deterministic DP54 rejection-quota failure: tight tolerances, a single fixed
|
||||
# step bound, and one allowed rejection. Every ray rejects its first trial step
|
||||
# and reports the exact REJECTION_LIMIT reason instead of a fabricated terminal
|
||||
# category.
|
||||
INJECT = ['--integrator', 'dp54',
|
||||
'--ode-rtol', '1e-15', '--ode-atol-x', '1e-15',
|
||||
'--ode-atol-pi', '1e-15', '--ode-atol-l', '1e-15',
|
||||
@@ -139,7 +141,7 @@ with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
|
||||
'--output', single)
|
||||
err = single_run.stderr
|
||||
assert 'Ray failures:' in err, err
|
||||
assert 'INCOMPLETE=' in err and 'INTEGRATION_ERROR' in err, err
|
||||
assert 'INCOMPLETE=' in err and 'REJECTION_LIMIT' in err, err
|
||||
assert frame_reported(err, 0), err
|
||||
if not is_debug(single_run):
|
||||
assert 'ray failure:' not in err, err
|
||||
@@ -154,7 +156,7 @@ with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
|
||||
'--output', verbose)
|
||||
err = verbose_run.stderr
|
||||
assert 'ray failure:' in err, err
|
||||
assert 'reason=INTEGRATION_ERROR' in err, err
|
||||
assert 'reason=REJECTION_LIMIT' in err, err
|
||||
assert 'vertex=' in err and 'film=(' in err, err
|
||||
assert 'accepted=' in err and 'rejected=' in err and 'rhs=' in err, err
|
||||
# Bounded detail: at most RAY_DIAG_REPS_PER_REASON per reason, remainder
|
||||
@@ -241,7 +243,7 @@ with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
|
||||
'--output', tmp / f'map_refused.{ext}', ok=False)
|
||||
err = refused.stderr
|
||||
assert frame_reported(err, 0) and frame_reported(err, 1), err
|
||||
assert 'INTEGRATION_ERROR' in err, err
|
||||
assert 'REJECTION_LIMIT' in err, err
|
||||
assert 'phase=import' in err, err
|
||||
assert 'Incomplete render refused' in err, err
|
||||
assert err.index('frame 0') < err.index('Incomplete render refused'), err
|
||||
@@ -255,7 +257,7 @@ with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
|
||||
'--verbose', '--output', replay)
|
||||
err = replay_run.stderr
|
||||
assert 'Ray failures:' in err and frame_reported(err, 0), err
|
||||
assert 'INTEGRATION_ERROR' in err and 'phase=import' in err, err
|
||||
assert 'REJECTION_LIMIT' in err and 'phase=import' in err, err
|
||||
assert 'stop_t=' not in err and 'trusted=1' not in err, err
|
||||
|
||||
# 6) Budget exhaustion is a distinct, always-on message, and its refusal
|
||||
|
||||
Reference in new issue
Block a user