Fix: Distinguish concrete ray failure causes

This commit is contained in:
wyj committed 2026-10-08 05:07:34 -04:00
1 parent 76ed705d37
commit 95c05f84d9
12 files changed
+580 -111

No files matched your search

+9
View File
@@ -790,6 +790,15 @@ ray-tracing 的 OpenMP worker 与 geodesic 热循环不写任何日志(既有
报告,与数值 `INCOMPLETE` 区分;replay 使用文件中保存的几何与顶点诊断,不虚构 报告,与数值 `INCOMPLETE` 区分;replay 使用文件中保存的几何与顶点诊断,不虚构
stop 状态,也不把未重试的旧失败当作新失败重复计数。 stop 状态,也不把未重试的旧失败当作新失败重复计数。
`RayReason` 的诊断粒度是追加式扩展:原 coarse reason 的数值 0..9 冻结不变,
更细的失败原因一律追加在 `RAY_REASON_IO_ERROR` 之后,`RAY_REASON_COUNT` 只作
计数/未知 sentinel 且不写入 wire。lens-map 的二进制布局(v2/v3)与字段偏移不
改变,新 reader 读旧值原样、读追加值按新名解释,并拒绝 `>= RAY_REASON_COUNT`;
旧 reader 因 coarse 校验仍会安全拒绝它不认识的新值。每个细原因通过纯函数
`ray_reason_category()` 归入既有 coarse 类别(PROTOCOL_ERROR、
INTEGRATION_ERROR、UNSUPPORTED、IO_ERROR 等),供只需要粗分类的调用方使用。
该扩展只细化失败状态的命名与上报,不改变任何物理终态判据、终止分类或 retry 规则。
--- ---
# 18A. 渐近外区、escape worldtube 与 endpoint 协议 # 18A. 渐近外区、escape worldtube 与 endpoint 协议
+1 -1
View File
@@ -1661,7 +1661,7 @@ int frame_lens_mesh_refine_with_progress(
endpoint = (RayEndpoint){.magnification = 1.0, endpoint = (RayEndpoint){.magnification = 1.0,
.end_id = SPACETIME_END_NONE, .end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE, .outcome = RAY_OUTCOME_INCOMPLETE,
.reason = RAY_REASON_PROTOCOL_ERROR, .reason = RAY_REASON_INVALID_CONTINUATION,
.stop_coordinate_time = NAN, .stop_coordinate_time = NAN,
.threshold_value = NAN}; .threshold_value = NAN};
} else { } else {
+220 -54
View File
@@ -31,10 +31,120 @@ const char *ray_reason_name(RayReason reason) {
return "PROTOCOL_ERROR"; return "PROTOCOL_ERROR";
case RAY_REASON_IO_ERROR: case RAY_REASON_IO_ERROR:
return "IO_ERROR"; return "IO_ERROR";
case RAY_REASON_INVALID_ARGUMENT:
return "INVALID_ARGUMENT";
case RAY_REASON_UNKNOWN_STEPPER:
return "UNKNOWN_STEPPER";
case RAY_REASON_INVALID_STEPPER_CONFIG:
return "INVALID_STEPPER_CONFIG";
case RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID:
return "ASYMPTOTIC_LIFECYCLE_INVALID";
case RAY_REASON_CAMERA_PREROUTE_FAILED:
return "CAMERA_PREROUTE_FAILED";
case RAY_REASON_INVALID_ROUTE_KIND:
return "INVALID_ROUTE_KIND";
case RAY_REASON_INVALID_CONTINUATION:
return "INVALID_CONTINUATION";
case RAY_REASON_METRIC_INTERNAL_ERROR:
return "METRIC_INTERNAL_ERROR";
case RAY_REASON_END_DESCRIPTOR_FAILED:
return "END_DESCRIPTOR_FAILED";
case RAY_REASON_WORLDTUBE_SAMPLE_FAILED:
return "WORLDTUBE_SAMPLE_FAILED";
case RAY_REASON_WORLDTUBE_GEOMETRY_INVALID:
return "WORLDTUBE_GEOMETRY_INVALID";
case RAY_REASON_WORLDTUBE_EVALUATION_FAILED:
return "WORLDTUBE_EVALUATION_FAILED";
case RAY_REASON_OUTSIDE_WORLDTUBE:
return "OUTSIDE_WORLDTUBE";
case RAY_REASON_ESCAPE_LOCALIZATION_FAILED:
return "ESCAPE_LOCALIZATION_FAILED";
case RAY_REASON_ESCAPE_TRANSFER_FAILED:
return "ESCAPE_TRANSFER_FAILED";
case RAY_REASON_INVALID_ESCAPE_DIRECTION:
return "INVALID_ESCAPE_DIRECTION";
case RAY_REASON_INVALID_STEP_INTERVAL:
return "INVALID_STEP_INTERVAL";
case RAY_REASON_TIME_STEP_UNREPRESENTABLE:
return "TIME_STEP_UNREPRESENTABLE";
case RAY_REASON_MIN_STEP_REACHED:
return "MIN_STEP_REACHED";
case RAY_REASON_REJECTION_LIMIT:
return "REJECTION_LIMIT";
case RAY_REASON_NONFINITE_TRIAL:
return "NONFINITE_TRIAL";
case RAY_REASON_SUBINTEGRATION_TARGET_INVALID:
return "SUBINTEGRATION_TARGET_INVALID";
case RAY_REASON_SUBINTEGRATION_TARGET_MISSED:
return "SUBINTEGRATION_TARGET_MISSED";
case RAY_REASON_ESCAPE_EVENT_UNCONFIRMED:
return "ESCAPE_EVENT_UNCONFIRMED";
case RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED:
return "THRESHOLD_EVENT_UNCONFIRMED";
case RAY_REASON_SLAB_LOAD_FAILED:
return "SLAB_LOAD_FAILED";
case RAY_REASON_COUNT:
break;
} }
return "UNKNOWN"; return "UNKNOWN";
} }
int ray_reason_valid(RayReason reason) {
return (unsigned)reason < (unsigned)RAY_REASON_COUNT;
}
RayReason ray_reason_category(RayReason reason) {
switch (reason) {
/* Frozen coarse reasons map to themselves. */
case RAY_REASON_NONE:
case RAY_REASON_REDSHIFT_LIMIT:
case RAY_REASON_BUDGET_EXHAUSTED:
case RAY_REASON_TIME_RANGE_EXHAUSTED:
case RAY_REASON_OUT_OF_DOMAIN:
case RAY_REASON_INVALID_METRIC:
case RAY_REASON_INTEGRATION_ERROR:
case RAY_REASON_UNSUPPORTED:
case RAY_REASON_PROTOCOL_ERROR:
case RAY_REASON_IO_ERROR:
return reason;
/* Protocol-derived detail reasons collapse onto PROTOCOL_ERROR. */
case RAY_REASON_INVALID_ARGUMENT:
case RAY_REASON_UNKNOWN_STEPPER:
case RAY_REASON_INVALID_STEPPER_CONFIG:
case RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID:
case RAY_REASON_CAMERA_PREROUTE_FAILED:
case RAY_REASON_INVALID_ROUTE_KIND:
case RAY_REASON_INVALID_CONTINUATION:
case RAY_REASON_METRIC_INTERNAL_ERROR:
case RAY_REASON_END_DESCRIPTOR_FAILED:
case RAY_REASON_WORLDTUBE_SAMPLE_FAILED:
case RAY_REASON_WORLDTUBE_GEOMETRY_INVALID:
case RAY_REASON_WORLDTUBE_EVALUATION_FAILED:
case RAY_REASON_OUTSIDE_WORLDTUBE:
case RAY_REASON_ESCAPE_LOCALIZATION_FAILED:
case RAY_REASON_ESCAPE_TRANSFER_FAILED:
return RAY_REASON_PROTOCOL_ERROR;
/* Integration-derived reasons, plus the legacy sky-direction failure that
* historically surfaced as an integration error. */
case RAY_REASON_INVALID_ESCAPE_DIRECTION:
case RAY_REASON_INVALID_STEP_INTERVAL:
case RAY_REASON_TIME_STEP_UNREPRESENTABLE:
case RAY_REASON_MIN_STEP_REACHED:
case RAY_REASON_REJECTION_LIMIT:
case RAY_REASON_NONFINITE_TRIAL:
case RAY_REASON_SUBINTEGRATION_TARGET_INVALID:
case RAY_REASON_SUBINTEGRATION_TARGET_MISSED:
case RAY_REASON_ESCAPE_EVENT_UNCONFIRMED:
case RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED:
return RAY_REASON_INTEGRATION_ERROR;
case RAY_REASON_SLAB_LOAD_FAILED:
return RAY_REASON_IO_ERROR;
case RAY_REASON_COUNT:
break;
}
return (RayReason)RAY_REASON_COUNT; /* documented UNKNOWN sentinel */
}
static double dot(const double a[3], const double b[3]) { static double dot(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
} }
@@ -477,25 +587,26 @@ static int dp_advance_one(const MetricSlab *slab,
const double t0 = s->coordinate_time; const double t0 = s->coordinate_time;
const double remaining = t0 - left; const double remaining = t0 - left;
if (!(remaining > 0.0) || !(proposal > 0.0)) { if (!(remaining > 0.0) || !(proposal > 0.0)) {
*reason = RAY_REASON_INTEGRATION_ERROR; *reason = RAY_REASON_INVALID_STEP_INTERVAL;
return -1; return -1;
} }
const int boundary_limited = remaining < proposal; const int boundary_limited = remaining < proposal;
double target = boundary_limited ? left : representable_target(t0, proposal); double target = boundary_limited ? left : representable_target(t0, proposal);
if (!(target < t0)) { if (!(target < t0)) {
*reason = RAY_REASON_INTEGRATION_ERROR; *reason = RAY_REASON_TIME_STEP_UNREPRESENTABLE;
return -1; return -1;
} }
double actual_h = t0 - target; double actual_h = t0 - target;
/* A boundary-limited final step may legitimately fall below min_step; any /* A boundary-limited final step may legitimately fall below min_step; any
* other sub-floor representable interval is an integration failure. */ * other sub-floor representable interval is an integration failure. */
if (!boundary_limited && actual_h < config->min_step) { if (!boundary_limited && actual_h < config->min_step) {
*reason = RAY_REASON_INTEGRATION_ERROR; *reason = RAY_REASON_MIN_STEP_REACHED;
return -1; return -1;
} }
unsigned int consecutive = 0; unsigned int consecutive = 0;
int rejected_any = 0; int rejected_any = 0;
int last_reject_stage = 0; int last_reject_stage = 0;
int last_reject_nonfinite = 0;
SpacetimePointStatus last_stage_status = SPACETIME_POINT_OK; SpacetimePointStatus last_stage_status = SPACETIME_POINT_OK;
while (1) { while (1) {
const State before = *s; const State before = *s;
@@ -559,9 +670,17 @@ static int dp_advance_one(const MetricSlab *slab,
rejected_any = 1; rejected_any = 1;
s->previous_rejected = 1; s->previous_rejected = 1;
last_reject_stage = stage_reject; last_reject_stage = stage_reject;
last_reject_nonfinite = (status == DP_TRIAL_NONFINITE);
if (++consecutive >= config->consecutive_rejection_limit) { if (++consecutive >= config->consecutive_rejection_limit) {
*reason = last_reject_stage ? reason_from_point_status(last_stage_status) /* A specific stage reason is more informative than the quota itself and
: RAY_REASON_INTEGRATION_ERROR; * is preserved; only a pure error-estimate rejection (or a nonfinite
* trial) reports the quota cause. */
if (last_reject_stage)
*reason = reason_from_point_status(last_stage_status);
else if (last_reject_nonfinite)
*reason = RAY_REASON_NONFINITE_TRIAL;
else
*reason = RAY_REASON_REJECTION_LIMIT;
return -1; return -1;
} }
if (factor > 1.0) if (factor > 1.0)
@@ -571,12 +690,12 @@ static int dp_advance_one(const MetricSlab *slab,
shrunk = remaining; shrunk = remaining;
target = (shrunk >= remaining) ? left : representable_target(t0, shrunk); target = (shrunk >= remaining) ? left : representable_target(t0, shrunk);
if (!(target < t0)) { if (!(target < t0)) {
*reason = RAY_REASON_INTEGRATION_ERROR; *reason = RAY_REASON_TIME_STEP_UNREPRESENTABLE;
return -1; return -1;
} }
actual_h = t0 - target; actual_h = t0 - target;
if (actual_h < config->min_step) { if (actual_h < config->min_step) {
*reason = RAY_REASON_INTEGRATION_ERROR; *reason = RAY_REASON_MIN_STEP_REACHED;
return -1; return -1;
} }
} }
@@ -653,17 +772,23 @@ typedef enum {
ASYM_LIFECYCLE_PROTOCOL_ERROR ASYM_LIFECYCLE_PROTOCOL_ERROR
} AsymLifecycleMode; } AsymLifecycleMode;
static AsymLifecycleMode asym_lifecycle_mode(const SpacetimeSource *source) { static AsymLifecycleMode asym_lifecycle_mode(const SpacetimeSource *source,
RayReason *failure) {
*failure = RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID;
const size_t count = spacetime_asymptotic_end_count(source); const size_t count = spacetime_asymptotic_end_count(source);
if (count == 0) if (count == 0)
return ASYM_LIFECYCLE_NONE; return ASYM_LIFECYCLE_NONE;
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
SpacetimeAsymptoticEnd end; SpacetimeAsymptoticEnd end;
if (spacetime_asymptotic_end(source, i, &end)) if (spacetime_asymptotic_end(source, i, &end)) {
*failure = RAY_REASON_END_DESCRIPTOR_FAILED;
return ASYM_LIFECYCLE_PROTOCOL_ERROR; return ASYM_LIFECYCLE_PROTOCOL_ERROR;
}
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI && if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI &&
end.exterior_kind != ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE) end.exterior_kind != ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE) {
*failure = RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID;
return ASYM_LIFECYCLE_PROTOCOL_ERROR; return ASYM_LIFECYCLE_PROTOCOL_ERROR;
}
} }
return ASYM_LIFECYCLE_READY; return ASYM_LIFECYCLE_READY;
} }
@@ -672,12 +797,18 @@ static AsymLifecycleMode asym_lifecycle_mode(const SpacetimeSource *source) {
* within one accepted step. Re-integrates from `before` with fractional step * within one accepted step. Re-integrates from `before` with fractional step
* sizes; `after` lands on the outside end of the bracket. Returns * sizes; `after` lands on the outside end of the bracket. Returns
* ASYMPTOTIC_OK, ASYMPTOTIC_TIME_RANGE_EXHAUSTED (a midpoint fell into a * ASYMPTOTIC_OK, ASYMPTOTIC_TIME_RANGE_EXHAUSTED (a midpoint fell into a
* history hole), or ASYMPTOTIC_INVALID. */ * history hole), or ASYMPTOTIC_INVALID. On a non-TIME_RANGE failure
* `*fail_reason` carries the classified cause: an RK4 subintegration failure
* (whose underlying metric reason is discarded by this collapsed path) is
* ESCAPE_LOCALIZATION_FAILED, while a worldtube value failure is
* WORLDTUBE_EVALUATION_FAILED. */
static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab, static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab,
SpacetimeEndId end_id, SpacetimeEndId end_id,
const State *before, const State *before,
double h, State *after, double h, State *after,
RayReason *fail_reason,
unsigned long *rhs_total) { unsigned long *rhs_total) {
*fail_reason = RAY_REASON_ESCAPE_LOCALIZATION_FAILED;
double f_lo = 0.0, f_hi = 1.0; double f_lo = 0.0, f_hi = 1.0;
for (int iteration = 0; iteration < 64; ++iteration) { for (int iteration = 0; iteration < 64; ++iteration) {
const double f = 0.5 * (f_lo + f_hi); const double f = 0.5 * (f_lo + f_hi);
@@ -688,8 +819,11 @@ static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab,
double value; double value;
const int status = asymptotic_worldtube_value( const int status = asymptotic_worldtube_value(
slab->source, end_id, mid.coordinate_time, mid.x, &value); slab->source, end_id, mid.coordinate_time, mid.x, &value);
if (status != ASYMPTOTIC_OK) if (status != ASYMPTOTIC_OK) {
if (status != ASYMPTOTIC_TIME_RANGE_EXHAUSTED)
*fail_reason = RAY_REASON_WORLDTUBE_EVALUATION_FAILED;
return (AsymptoticStatus)status; return (AsymptoticStatus)status;
}
if (value >= 0.0) if (value >= 0.0)
f_hi = f; f_hi = f;
else else
@@ -732,7 +866,7 @@ static int dp_subintegrate(const MetricSlab *slab,
return 0; return 0;
} }
if (!(t_target < before->coordinate_time)) { if (!(t_target < before->coordinate_time)) {
*reason_out = RAY_REASON_INTEGRATION_ERROR; *reason_out = RAY_REASON_SUBINTEGRATION_TARGET_INVALID;
return -1; return -1;
} }
State work = *before; State work = *before;
@@ -755,7 +889,7 @@ static int dp_subintegrate(const MetricSlab *slab,
work.coordinate_time = t_target; work.coordinate_time = t_target;
} }
if (fabs(work.coordinate_time - t_target) > snap) { if (fabs(work.coordinate_time - t_target) > snap) {
*reason_out = RAY_REASON_INTEGRATION_ERROR; *reason_out = RAY_REASON_SUBINTEGRATION_TARGET_MISSED;
return -1; return -1;
} }
work.coordinate_time = t_target; work.coordinate_time = t_target;
@@ -789,7 +923,7 @@ static AsymptoticStatus dp_localize_worldtube_crossing(
const double span = t_end - t0; const double span = t_end - t0;
const double stop_tol = const double stop_tol =
fmax(1e-12 * fabs(span), 2.0 * time_ulp(t0)); fmax(1e-12 * fabs(span), 2.0 * time_ulp(t0));
*integration_reason = RAY_REASON_PROTOCOL_ERROR; *integration_reason = RAY_REASON_WORLDTUBE_EVALUATION_FAILED;
for (int iteration = 0; iteration < 256; ++iteration) { for (int iteration = 0; iteration < 256; ++iteration) {
if (fabs(span) * (f_hi - f_lo) <= stop_tol) if (fabs(span) * (f_hi - f_lo) <= stop_tol)
break; break;
@@ -1223,25 +1357,34 @@ static int dp_localize_threshold_bracket(const MetricSlab *slab,
* the step so it cannot cross a motion-segment boundary (which would break the * 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 * constant-velocity polynomial used by the dense event layer). On success
* *event_left is the tightened left bound; on TIME_RANGE_EXHAUSTED * *event_left is the tightened left bound; on TIME_RANGE_EXHAUSTED
* *time_range_end carries the end that exhausted its history. */ * *time_range_end carries the end that exhausted its history. On
* ASYMPTOTIC_INVALID `*failure` carries the classified cause
* (END_DESCRIPTOR_FAILED / WORLDTUBE_SAMPLE_FAILED / WORLDTUBE_GEOMETRY_INVALID). */
static AsymptoticStatus worldtube_prestep_scan( static AsymptoticStatus worldtube_prestep_scan(
const SpacetimeSource *source, size_t end_count, double t, double step_left, const SpacetimeSource *source, size_t end_count, double t, double step_left,
double *event_left, SpacetimeEndId *time_range_end) { double *event_left, SpacetimeEndId *time_range_end, RayReason *failure) {
*event_left = step_left; *event_left = step_left;
*failure = RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID;
for (size_t i = 0; i < end_count; ++i) { for (size_t i = 0; i < end_count; ++i) {
SpacetimeAsymptoticEnd end; SpacetimeAsymptoticEnd end;
if (spacetime_asymptotic_end(source, i, &end)) if (spacetime_asymptotic_end(source, i, &end)) {
*failure = RAY_REASON_END_DESCRIPTOR_FAILED;
return ASYMPTOTIC_INVALID; return ASYMPTOTIC_INVALID;
}
SpacetimeEscapeWorldtubeSample sample; SpacetimeEscapeWorldtubeSample sample;
if (spacetime_escape_worldtube_sample(source, end.end_id, t, &sample)) if (spacetime_escape_worldtube_sample(source, end.end_id, t, &sample)) {
*failure = RAY_REASON_WORLDTUBE_SAMPLE_FAILED;
return ASYMPTOTIC_INVALID; return ASYMPTOTIC_INVALID;
}
if (!sample.valid) { if (!sample.valid) {
*time_range_end = end.end_id; *time_range_end = end.end_id;
return ASYMPTOTIC_TIME_RANGE_EXHAUSTED; return ASYMPTOTIC_TIME_RANGE_EXHAUSTED;
} }
if (!(sample.radius > 0.0) || !isfinite(sample.radius) || if (!(sample.radius > 0.0) || !isfinite(sample.radius) ||
!isfinite(sample.radius_rate)) !isfinite(sample.radius_rate)) {
*failure = RAY_REASON_WORLDTUBE_GEOMETRY_INVALID;
return ASYMPTOTIC_INVALID; return ASYMPTOTIC_INVALID;
}
if (!sample.velocity_constant) if (!sample.velocity_constant)
return ASYMPTOTIC_UNSUPPORTED; return ASYMPTOTIC_UNSUPPORTED;
const double boundary = const double boundary =
@@ -1284,7 +1427,7 @@ static RayReason reason_from_point_status(SpacetimePointStatus status) {
case SPACETIME_POINT_INVALID_METRIC: case SPACETIME_POINT_INVALID_METRIC:
return RAY_REASON_INVALID_METRIC; return RAY_REASON_INVALID_METRIC;
case SPACETIME_POINT_INTERNAL_ERROR: case SPACETIME_POINT_INTERNAL_ERROR:
return RAY_REASON_PROTOCOL_ERROR; return RAY_REASON_METRIC_INTERNAL_ERROR;
default: default:
return RAY_REASON_INTEGRATION_ERROR; return RAY_REASON_INTEGRATION_ERROR;
} }
@@ -1370,11 +1513,12 @@ static int threshold_reached(const MetricSlab *slab,
/* Legacy no-declared-ends backends may still report a region escape. There /* Legacy no-declared-ends backends may still report a region escape. There
* is no position-based physical capture; a failed sky direction is reported * is no position-based physical capture; a failed sky direction is reported
* as an integration failure, never as capture. */ * as an invalid escape direction (historically an integration failure), never
* as capture. */
static GeodesicAdvanceResult legacy_escape(const MetricSlab *slab, static GeodesicAdvanceResult legacy_escape(const MetricSlab *slab,
const State *s, RayEndpoint *out) { const State *s, RayEndpoint *out) {
if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) != 0) { if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) != 0) {
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); set_incomplete(out, RAY_REASON_INVALID_ESCAPE_DIRECTION, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
out->outcome = RAY_OUTCOME_ESCAPED; out->outcome = RAY_OUTCOME_ESCAPED;
@@ -1392,7 +1536,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
slab_left_time > s->coordinate_time) { slab_left_time > s->coordinate_time) {
if (out != NULL) { if (out != NULL) {
out->outcome = RAY_OUTCOME_INCOMPLETE; out->outcome = RAY_OUTCOME_INCOMPLETE;
out->reason = RAY_REASON_PROTOCOL_ERROR; out->reason = RAY_REASON_INVALID_ARGUMENT;
record_final_state(out, s); record_final_state(out, s);
out->threshold_value = NAN; out->threshold_value = NAN;
} }
@@ -1401,7 +1545,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
/* An unknown stepper code must be rejected explicitly, exactly like an /* An unknown stepper code must be rejected explicitly, exactly like an
* unknown wire code; "not DP54" must never fall through to the RK4 path. */ * unknown wire code; "not DP54" must never fall through to the RK4 path. */
if (!stepper_known(config->stepper)) { if (!stepper_known(config->stepper)) {
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_UNKNOWN_STEPPER, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; const int use_dp = config->stepper == GEODESIC_STEPPER_DP54;
@@ -1412,16 +1556,18 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
out->accepted_step_limit = config->max_steps; out->accepted_step_limit = config->max_steps;
if (use_dp) { if (use_dp) {
if (!dp_config_valid(config)) { if (!dp_config_valid(config)) {
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_INVALID_STEPPER_CONFIG, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
} else if (config->coordinate_time_step <= 0 || !config->max_steps) { } else if (config->coordinate_time_step <= 0 || !config->max_steps) {
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_INVALID_STEPPER_CONFIG, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
const AsymLifecycleMode mode = asym_lifecycle_mode(slab->source); RayReason lifecycle_reason = RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID;
const AsymLifecycleMode mode =
asym_lifecycle_mode(slab->source, &lifecycle_reason);
if (mode == ASYM_LIFECYCLE_PROTOCOL_ERROR) { if (mode == ASYM_LIFECYCLE_PROTOCOL_ERROR) {
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, lifecycle_reason, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
const int directed = mode == ASYM_LIFECYCLE_READY; const int directed = mode == ASYM_LIFECYCLE_READY;
@@ -1467,9 +1613,10 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
double event_left = step_left; double event_left = step_left;
if (directed) { if (directed) {
SpacetimeEndId tr_end = SPACETIME_END_NONE; SpacetimeEndId tr_end = SPACETIME_END_NONE;
RayReason scan_failure = RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID;
const AsymptoticStatus scan = worldtube_prestep_scan( const AsymptoticStatus scan = worldtube_prestep_scan(
slab->source, end_count, s->coordinate_time, step_left, &event_left, slab->source, end_count, s->coordinate_time, step_left, &event_left,
&tr_end); &tr_end, &scan_failure);
if (scan == ASYMPTOTIC_UNSUPPORTED) { if (scan == ASYMPTOTIC_UNSUPPORTED) {
set_incomplete(out, RAY_REASON_UNSUPPORTED, s); set_incomplete(out, RAY_REASON_UNSUPPORTED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
@@ -1480,7 +1627,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
return GEODESIC_ADVANCE_TERMINATED; return GEODESIC_ADVANCE_TERMINATED;
} }
if (scan != ASYMPTOTIC_OK) { if (scan != ASYMPTOTIC_OK) {
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, scan_failure, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
} }
@@ -1506,7 +1653,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
SpacetimeAsymptoticEnd end; SpacetimeAsymptoticEnd end;
if (spacetime_asymptotic_end(slab->source, i, &end)) { if (spacetime_asymptotic_end(slab->source, i, &end)) {
restore_accepted_trajectory(s, &before); restore_accepted_trajectory(s, &before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_END_DESCRIPTOR_FAILED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
double f_before, f_after; double f_before, f_after;
@@ -1524,14 +1671,16 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
} }
if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) { if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) {
restore_accepted_trajectory(s, &before); restore_accepted_trajectory(s, &before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_WORLDTUBE_EVALUATION_FAILED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
if (f_before > 0.0 || f_after <= 0.0) if (f_before > 0.0 || f_after <= 0.0)
continue; continue;
State crossing; State crossing;
RayReason locate_failure = RAY_REASON_ESCAPE_LOCALIZATION_FAILED;
const AsymptoticStatus localized = localize_worldtube_crossing( const AsymptoticStatus localized = localize_worldtube_crossing(
slab, end.end_id, &before, h, &crossing, &rhs_total); slab, end.end_id, &before, h, &crossing, &locate_failure,
&rhs_total);
s->rhs_evaluations = rhs_total; s->rhs_evaluations = rhs_total;
s->rejected_steps = reject_total; s->rejected_steps = reject_total;
if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) { if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
@@ -1542,7 +1691,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
} }
if (localized != ASYMPTOTIC_OK) { if (localized != ASYMPTOTIC_OK) {
restore_accepted_trajectory(s, &before); restore_accepted_trajectory(s, &before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, locate_failure, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
const AsymptoticStatus transfer = asymptotic_finish_escape( const AsymptoticStatus transfer = asymptotic_finish_escape(
@@ -1567,7 +1716,10 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
return GEODESIC_ADVANCE_TERMINATED; return GEODESIC_ADVANCE_TERMINATED;
} }
restore_accepted_trajectory(s, &before); restore_accepted_trajectory(s, &before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, transfer == ASYMPTOTIC_UNSUPPORTED
? RAY_REASON_UNSUPPORTED
: RAY_REASON_ESCAPE_TRANSFER_FAILED,
s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
continue; continue;
@@ -1615,7 +1767,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
SpacetimeAsymptoticEnd end; SpacetimeAsymptoticEnd end;
if (spacetime_asymptotic_end(slab->source, i, &end)) { if (spacetime_asymptotic_end(slab->source, i, &end)) {
restore_accepted_trajectory(s, &step_before); restore_accepted_trajectory(s, &step_before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_END_DESCRIPTOR_FAILED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
double f_before, f_after; double f_before, f_after;
@@ -1658,7 +1810,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
* a multi-end ray legitimately sits outside some ends. */ * a multi-end ray legitimately sits outside some ends. */
if (end_count == 1) { if (end_count == 1) {
restore_accepted_trajectory(s, &step_before); restore_accepted_trajectory(s, &step_before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_OUTSIDE_WORLDTUBE, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
continue; continue;
@@ -1732,14 +1884,14 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
if (event_retry_limit(s, config, &step_before, &event_consecutive, if (event_retry_limit(s, config, &step_before, &event_consecutive,
&reject_total)) { &reject_total)) {
s->rhs_evaluations = rhs_total; s->rhs_evaluations = rhs_total;
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); set_incomplete(out, RAY_REASON_ESCAPE_EVENT_UNCONFIRMED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
retry_step = 1; retry_step = 1;
break; break;
} }
State crossing; State crossing;
RayReason event_reason = RAY_REASON_PROTOCOL_ERROR; RayReason event_reason = RAY_REASON_WORLDTUBE_EVALUATION_FAILED;
const AsymptoticStatus localized = dp_localize_worldtube_crossing( const AsymptoticStatus localized = dp_localize_worldtube_crossing(
slab, config, end.end_id, &step_before, h, b_lo, b_hi, &crossing, slab, config, end.end_id, &step_before, h, b_lo, b_hi, &crossing,
&event_reason, &rhs_total, &reject_total); &event_reason, &rhs_total, &reject_total);
@@ -1776,7 +1928,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
} }
if (end_fail != ASYMPTOTIC_OK) { if (end_fail != ASYMPTOTIC_OK) {
restore_accepted_trajectory(s, &step_before); restore_accepted_trajectory(s, &step_before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, RAY_REASON_WORLDTUBE_EVALUATION_FAILED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
if (retry_step) if (retry_step)
@@ -1849,7 +2001,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
if (event_retry_limit(s, config, &step_before, &event_consecutive, if (event_retry_limit(s, config, &step_before, &event_consecutive,
&reject_total)) { &reject_total)) {
s->rhs_evaluations = rhs_total; s->rhs_evaluations = rhs_total;
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); set_incomplete(out, RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
continue; continue;
@@ -1870,7 +2022,7 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
if (event_retry_limit(s, config, &step_before, &event_consecutive, if (event_retry_limit(s, config, &step_before, &event_consecutive,
&reject_total)) { &reject_total)) {
s->rhs_evaluations = rhs_total; s->rhs_evaluations = rhs_total;
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s); set_incomplete(out, RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED, s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
continue; continue;
@@ -1932,7 +2084,10 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
return GEODESIC_ADVANCE_TERMINATED; return GEODESIC_ADVANCE_TERMINATED;
} }
restore_accepted_trajectory(s, &step_before); restore_accepted_trajectory(s, &step_before);
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s); set_incomplete(out, transfer == ASYMPTOTIC_UNSUPPORTED
? RAY_REASON_UNSUPPORTED
: RAY_REASON_ESCAPE_TRANSFER_FAILED,
s);
return GEODESIC_ADVANCE_FAILED; return GEODESIC_ADVANCE_FAILED;
} }
} }
@@ -1971,21 +2126,29 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
.accepted_steps = 0, .accepted_steps = 0,
.threshold_value = NAN}; .threshold_value = NAN};
record_final_state(&out, NULL); record_final_state(&out, NULL);
if (!source || !observer || !config || fabs(dot(n, n) - 1) > 1e-10) if (!source || !observer || !config || n == NULL) {
out.reason = RAY_REASON_INVALID_ARGUMENT;
return out; return out;
}
const double direction_norm2 = dot(n, n);
if (!isfinite(direction_norm2) || fabs(direction_norm2 - 1.0) > 1e-10) {
out.reason = RAY_REASON_INVALID_ARGUMENT;
return out;
}
if (!stepper_known(config->stepper)) { if (!stepper_known(config->stepper)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_UNKNOWN_STEPPER;
return out; return out;
} }
const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; const int use_dp = config->stepper == GEODESIC_STEPPER_DP54;
if (use_dp) { if (use_dp) {
if (!dp_config_valid(config)) { if (!dp_config_valid(config)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_INVALID_STEPPER_CONFIG;
return out; return out;
} }
} else if (config->coordinate_time_step <= 0 || !config->max_steps) { } else if (config->coordinate_time_step <= 0 || !config->max_steps) {
out.reason = RAY_REASON_INVALID_STEPPER_CONFIG;
return out; return out;
} }
out.lookback_limit = use_dp ? config->max_lookback_time : 0.0; out.lookback_limit = use_dp ? config->max_lookback_time : 0.0;
@@ -2006,7 +2169,7 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
} }
if (route_status != ASYMPTOTIC_OK) { if (route_status != ASYMPTOTIC_OK) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_CAMERA_PREROUTE_FAILED;
return out; return out;
} }
if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) { if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) {
@@ -2027,7 +2190,7 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE && if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
route.kind != ASYMPTOTIC_ROUTE_ENTRY) { route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_INVALID_ROUTE_KIND;
return out; return out;
} }
State state = {.coordinate_time = route.activate_t, State state = {.coordinate_time = route.activate_t,
@@ -2055,7 +2218,7 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
MetricSlab *slab = NULL; MetricSlab *slab = NULL;
if (spacetime_load_slab(source, route.activate_t, last_time - 1.0, &slab)) { if (spacetime_load_slab(source, route.activate_t, last_time - 1.0, &slab)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_IO_ERROR; out.reason = RAY_REASON_SLAB_LOAD_FAILED;
return out; return out;
} }
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) == if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
@@ -2081,21 +2244,24 @@ RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
.threshold_value = NAN}; .threshold_value = NAN};
record_final_state(&out, NULL); record_final_state(&out, NULL);
if (!source || !state_in || !config || !config->max_steps || if (!source || !state_in || !config || !config->max_steps ||
state_in->steps >= config->max_steps) state_in->steps >= config->max_steps) {
out.reason = RAY_REASON_INVALID_ARGUMENT;
return out; return out;
}
if (!stepper_known(config->stepper)) { if (!stepper_known(config->stepper)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_UNKNOWN_STEPPER;
return out; return out;
} }
const int use_dp = config->stepper == GEODESIC_STEPPER_DP54; const int use_dp = config->stepper == GEODESIC_STEPPER_DP54;
if (use_dp) { if (use_dp) {
if (!dp_config_valid(config)) { if (!dp_config_valid(config)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_PROTOCOL_ERROR; out.reason = RAY_REASON_INVALID_STEPPER_CONFIG;
return out; return out;
} }
} else if (config->coordinate_time_step <= 0) { } else if (config->coordinate_time_step <= 0) {
out.reason = RAY_REASON_INVALID_STEPPER_CONFIG;
return out; return out;
} }
out.lookback_limit = use_dp ? config->max_lookback_time : 0.0; out.lookback_limit = use_dp ? config->max_lookback_time : 0.0;
@@ -2115,7 +2281,7 @@ RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
MetricSlab *slab = NULL; MetricSlab *slab = NULL;
if (spacetime_load_slab(source, state.coordinate_time, last_time, &slab)) { if (spacetime_load_slab(source, state.coordinate_time, last_time, &slab)) {
out.outcome = RAY_OUTCOME_INCOMPLETE; out.outcome = RAY_OUTCOME_INCOMPLETE;
out.reason = RAY_REASON_IO_ERROR; out.reason = RAY_REASON_SLAB_LOAD_FAILED;
return out; return out;
} }
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) == if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
+56 -2
View File
@@ -14,7 +14,15 @@ typedef enum {
} RayOutcome; } RayOutcome;
/* Diagnostic reason. Different DARK reasons must not create a mesh seam; the /* Diagnostic reason. Different DARK reasons must not create a mesh seam; the
* reason is for accounting and provenance only. */ * reason is for accounting and provenance only.
*
* Wire compatibility: the original coarse reasons keep their frozen numeric
* codes 0..9 exactly. Every appended detail reason therefore has a larger
* numeric value, and an older reader that validates `reason > RAY_REASON_IO_ERROR`
* safely rejects a map produced with a detail reason instead of silently
* reinterpreting it. A current reader accepts the frozen 0..9 codes and the
* appended detail codes, and rejects anything at or above RAY_REASON_COUNT.
* RAY_REASON_COUNT is a sentinel/count, never a serialized wire value. */
typedef enum { typedef enum {
RAY_REASON_NONE = 0, RAY_REASON_NONE = 0,
RAY_REASON_REDSHIFT_LIMIT, RAY_REASON_REDSHIFT_LIMIT,
@@ -25,13 +33,59 @@ typedef enum {
RAY_REASON_INTEGRATION_ERROR, RAY_REASON_INTEGRATION_ERROR,
RAY_REASON_UNSUPPORTED, RAY_REASON_UNSUPPORTED,
RAY_REASON_PROTOCOL_ERROR, RAY_REASON_PROTOCOL_ERROR,
RAY_REASON_IO_ERROR RAY_REASON_IO_ERROR,
/* ---- Appended detail reasons (ids > RAY_REASON_IO_ERROR). ---- */
/* Protocol-derived. */
RAY_REASON_INVALID_ARGUMENT, /* null/nonunit/bad left bound */
RAY_REASON_UNKNOWN_STEPPER, /* unrecognized stepper code */
RAY_REASON_INVALID_STEPPER_CONFIG, /* stepper config rejected */
RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID, /* declared ends inconsistent */
RAY_REASON_CAMERA_PREROUTE_FAILED, /* preroute returned INVALID */
RAY_REASON_INVALID_ROUTE_KIND, /* unknown pre-route kind */
RAY_REASON_INVALID_CONTINUATION, /* retry without valid payload */
RAY_REASON_METRIC_INTERNAL_ERROR, /* SPACETIME_POINT_INTERNAL_ERROR */
RAY_REASON_END_DESCRIPTOR_FAILED, /* end descriptor retrieval failed */
RAY_REASON_WORLDTUBE_SAMPLE_FAILED, /* worldtube sample callback failed */
RAY_REASON_WORLDTUBE_GEOMETRY_INVALID, /* nonfinite/nonpositive radius/rate */
RAY_REASON_WORLDTUBE_EVALUATION_FAILED, /* worldtube value eval invalid */
RAY_REASON_OUTSIDE_WORLDTUBE, /* single-end pre-step F > geom_tol */
RAY_REASON_ESCAPE_LOCALIZATION_FAILED, /* crossing localizer invalid */
RAY_REASON_ESCAPE_TRANSFER_FAILED, /* finish_escape returned invalid */
RAY_REASON_INVALID_ESCAPE_DIRECTION, /* legacy sky normalize invalid */
/* Integration-derived. */
RAY_REASON_INVALID_STEP_INTERVAL, /* remaining/proposal nonpositive */
RAY_REASON_TIME_STEP_UNREPRESENTABLE, /* target >= t0 after rounding */
RAY_REASON_MIN_STEP_REACHED, /* actual_h < min_step */
RAY_REASON_REJECTION_LIMIT, /* error-estimate rejections exhausted */
RAY_REASON_NONFINITE_TRIAL, /* nonfinite trial hit reject quota */
RAY_REASON_SUBINTEGRATION_TARGET_INVALID,/* subintegrate target in future */
RAY_REASON_SUBINTEGRATION_TARGET_MISSED, /* subintegr. discrepancy > snap */
RAY_REASON_ESCAPE_EVENT_UNCONFIRMED, /* escape event retry exhausted */
RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED, /* threshold event retry exhausted */
/* I/O-derived. */
RAY_REASON_SLAB_LOAD_FAILED, /* spacetime_load_slab failed */
RAY_REASON_COUNT /* sentinel: valid ids are < COUNT */
} RayReason; } RayReason;
/* Stable name for a RayReason (never NULL; out-of-range values return /* Stable name for a RayReason (never NULL; out-of-range values return
* "UNKNOWN"). Pure and thread-safe, so it may be called from any reporter. */ * "UNKNOWN"). Pure and thread-safe, so it may be called from any reporter. */
const char *ray_reason_name(RayReason reason); const char *ray_reason_name(RayReason reason);
/* Nonzero when `reason` is one of the valid wire/provenance codes
* (0 <= reason < RAY_REASON_COUNT). Pure and thread-safe. */
int ray_reason_valid(RayReason reason);
/* Coarse category of a reason, for callers that only need the historical
* bucket. The frozen coarse reasons 0..9 map to themselves; each appended
* detail reason maps onto one of the existing coarse categories
* (PROTOCOL_ERROR / INTEGRATION_ERROR / UNSUPPORTED / IO_ERROR).
* ESCAPE_LOCALIZATION_FAILED historically surfaced as PROTOCOL_ERROR (the RK4
* crossing localizer collapsed to ASYMPTOTIC_INVALID at its caller) and
* INVALID_ESCAPE_DIRECTION historically surfaced as INTEGRATION_ERROR.
* Out-of-range values return the documented sentinel RAY_REASON_COUNT. Pure
* and thread-safe. */
RayReason ray_reason_category(RayReason reason);
/* Monitored quantity used by the dark-redshift termination policy. `LOG_P0` /* Monitored quantity used by the dark-redshift termination policy. `LOG_P0`
* is ln(p^0) = L - ln(alpha); it differs from `LOG_ALPHA_P0` by a local * is ln(p^0) = L - ln(alpha); it differs from `LOG_ALPHA_P0` by a local
* function of position and must not be confused with the true infinity * function of position and must not be confused with the true infinity
+2 -2
View File
@@ -123,7 +123,7 @@ static int valid_mesh(const FrameLensMesh *m) {
for (size_t i = 0; i < m->vertex_count; ++i) { for (size_t i = 0; i < m->vertex_count; ++i) {
const LensVertex *v = &m->vertices[i]; const LensVertex *v = &m->vertices[i];
if (!v->traced || v->outcome > RAY_OUTCOME_INCOMPLETE || if (!v->traced || v->outcome > RAY_OUTCOME_INCOMPLETE ||
v->reason > RAY_REASON_IO_ERROR || !isfinite(v->image_x) || !ray_reason_valid(v->reason) || !isfinite(v->image_x) ||
!isfinite(v->image_y) || !isfinite(v->log_frequency_ratio) || !isfinite(v->image_y) || !isfinite(v->log_frequency_ratio) ||
!unit_vector(v->camera_direction)) !unit_vector(v->camera_direction))
return 0; return 0;
@@ -296,7 +296,7 @@ int lens_map_read(const char *path, LensMapProvenance *provenance,
failed = failed || read_double(file, &v->log_frequency_ratio, &crc) || failed = failed || read_double(file, &v->log_frequency_ratio, &crc) ||
read_u32(file, &end_id, &crc) || read_u32(file, &outcome, &crc) || read_u32(file, &end_id, &crc) || read_u32(file, &outcome, &crc) ||
read_u32(file, &reason, &crc) || outcome > RAY_OUTCOME_INCOMPLETE || read_u32(file, &reason, &crc) || outcome > RAY_OUTCOME_INCOMPLETE ||
reason > RAY_REASON_IO_ERROR; !ray_reason_valid((RayReason)reason);
v->end_id = (SpacetimeEndId)end_id; v->end_id = (SpacetimeEndId)end_id;
v->outcome = (RayOutcome)outcome; v->outcome = (RayOutcome)outcome;
v->reason = (RayReason)reason; v->reason = (RayReason)reason;
+10 -9
View File
@@ -974,7 +974,7 @@ static void ray_pool_status_counts(const RayPool *rays, size_t *pending,
* result visible with its reason and frame. * result visible with its reason and frame.
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
#define RAY_DIAG_REASON_UNKNOWN_INDEX ((size_t)RAY_REASON_IO_ERROR + 1u) #define RAY_DIAG_REASON_UNKNOWN_INDEX ((size_t)RAY_REASON_COUNT)
#define RAY_DIAG_REASON_COUNT (RAY_DIAG_REASON_UNKNOWN_INDEX + 1u) #define RAY_DIAG_REASON_COUNT (RAY_DIAG_REASON_UNKNOWN_INDEX + 1u)
#define RAY_DIAG_REPS_PER_REASON 3u #define RAY_DIAG_REPS_PER_REASON 3u
@@ -1076,7 +1076,7 @@ static void raydiag_report_flush(const RayDiagReport *report, size_t frame_id,
long long generation, long long slab) { long long generation, long long slab) {
if (report->total == 0) if (report->total == 0)
return; return;
fprintf(stderr, "Ray failures: frame %zu camera_t=%.9g phase=%s", frame_id, fprintf(stderr, "Ray failures: frame %zu camera_t=%.17g phase=%s", frame_id,
camera_time, phase); camera_time, phase);
if (generation >= 0) if (generation >= 0)
fprintf(stderr, " generation=%lld", generation); fprintf(stderr, " generation=%lld", generation);
@@ -1108,12 +1108,12 @@ static void raydiag_report_flush(const RayDiagReport *report, size_t frame_id,
if (d->has_persistent) if (d->has_persistent)
fprintf(stderr, " vertex=%zu", d->persistent_id); fprintf(stderr, " vertex=%zu", d->persistent_id);
if (d->has_film) if (d->has_film)
fprintf(stderr, " film=(%.6g, %.6g)", d->film_x, d->film_y); fprintf(stderr, " film=(%.17g, %.17g)", d->film_x, d->film_y);
fprintf(stderr, " trusted=%d", d->trusted); fprintf(stderr, " trusted=%d", d->trusted);
if (d->trusted) { if (d->trusted) {
fprintf(stderr, " stop_t=%.9g", d->stop_time); fprintf(stderr, " stop_t=%.17g", d->stop_time);
if (d->has_position) if (d->has_position)
fprintf(stderr, " pos=(%.9g, %.9g, %.9g)", d->position[0], fprintf(stderr, " pos=(%.17g, %.17g, %.17g)", d->position[0],
d->position[1], d->position[2]); d->position[1], d->position[2]);
} }
fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n", d->accepted, fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n", d->accepted,
@@ -1349,7 +1349,7 @@ static void report_unresolved_frame(const Settings *s,
mesh->vertices[i].outcome == RAY_OUTCOME_UNRESOLVED) mesh->vertices[i].outcome == RAY_OUTCOME_UNRESOLVED)
++unresolved_vertices; ++unresolved_vertices;
fprintf(stderr, fprintf(stderr,
"UNRESOLVED/BUDGET_EXHAUSTED: frame %zu camera_t=%.9g " "UNRESOLVED/BUDGET_EXHAUSTED: frame %zu camera_t=%.17g "
"blocking_triangles=%zu unresolved_samples=%zu\n", "blocking_triangles=%zu unresolved_samples=%zu\n",
frame_id, camera_time, stats->budget_incomplete_triangles, frame_id, camera_time, stats->budget_incomplete_triangles,
unresolved_vertices); unresolved_vertices);
@@ -1362,11 +1362,11 @@ static void report_unresolved_frame(const Settings *s,
if (!v->traced || v->outcome != RAY_OUTCOME_UNRESOLVED) if (!v->traced || v->outcome != RAY_OUTCOME_UNRESOLVED)
continue; continue;
fprintf(stderr, fprintf(stderr,
" unresolved sample: frame=%zu vertex=%zu film=(%.6g, %.6g) " " unresolved sample: frame=%zu vertex=%zu film=(%.17g, %.17g) "
"continuation_t=", "continuation_t=",
frame_id, i, v->image_x, v->image_y); frame_id, i, v->image_x, v->image_y);
if (continuation_valid) if (continuation_valid)
fprintf(stderr, "%.9g", v->continuation_t); fprintf(stderr, "%.17g", v->continuation_t);
else else
fputs("-", stderr); fputs("-", stderr);
fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n", fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n",
@@ -2201,7 +2201,8 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
if (frame_vertex_continuation_state(&fs->vertex, &state)) { if (frame_vertex_continuation_state(&fs->vertex, &state)) {
fprintf(stderr, fprintf(stderr,
"Ray trace generation %zu: frame %zu sample %zu retry is " "Ray trace generation %zu: frame %zu sample %zu retry is "
"missing a valid continuation payload.\n", "missing a valid continuation payload "
"(reason=INVALID_CONTINUATION).\n",
generation, movie->frames[f].frame_id, sample); generation, movie->frames[f].frame_id, sample);
free(reported); free(reported);
ray_pool_destroy(&rays); ray_pool_destroy(&rays);
+2 -2
View File
@@ -150,7 +150,7 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE; p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED
? RAY_REASON_UNSUPPORTED ? RAY_REASON_UNSUPPORTED
: RAY_REASON_PROTOCOL_ERROR; : RAY_REASON_CAMERA_PREROUTE_FAILED;
p->endpoint[i].end_id = route.end_id; p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_FAILED; p->status[i] = RAY_POOL_FAILED;
continue; continue;
@@ -182,7 +182,7 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE && if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
route.kind != ASYMPTOTIC_ROUTE_ENTRY) { route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE; p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_PROTOCOL_ERROR; p->endpoint[i].reason = RAY_REASON_INVALID_ROUTE_KIND;
p->status[i] = RAY_POOL_FAILED; p->status[i] = RAY_POOL_FAILED;
continue; continue;
} }
+52 -1
View File
@@ -499,11 +499,61 @@ static void test_end_protocol_error(void) {
CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) == CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) ==
GEODESIC_ADVANCE_FAILED && GEODESIC_ADVANCE_FAILED &&
endpoint.outcome == RAY_OUTCOME_INCOMPLETE && 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"); "advance rejects a declared-but-broken end without legacy");
spacetime_free_slab(slab); 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) { static void test_interior_crossing_bisection_failure(void) {
/* radius 20.3 makes the exit land strictly between steps: the accepted /* 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 * 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_motion_segment_domain();
test_schwarzschild_sample_failures(); test_schwarzschild_sample_failures();
test_end_protocol_error(); test_end_protocol_error();
test_advance_worldtube_failure_reasons();
test_nonconstant_preroute_unsupported(); test_nonconstant_preroute_unsupported();
test_piecewise_constant_history_hole(); test_piecewise_constant_history_hole();
test_interior_history_exhaustion(); test_interior_history_exhaustion();
+41
View File
@@ -1418,6 +1418,10 @@ int main(void) {
.outcome = RAY_OUTCOME_DARK, .outcome = RAY_OUTCOME_DARK,
.reason = RAY_REASON_REDSHIFT_LIMIT, .reason = RAY_REASON_REDSHIFT_LIMIT,
.end_id = SPACETIME_END_NONE, .traced = 1}; .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_accepted_steps = 11; dv[0].trace_rejected_steps = 2;
dv[0].trace_rhs_evaluations = 79; dv[0].trace_rhs_evaluations = 79;
dv[1].trace_accepted_steps = 5; 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_rejected_steps != 2 ||
dloaded.frames[0].mesh.vertices[0].trace_rhs_evaluations != 79 || 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[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) { dloaded.frames[0].mesh.triangles[0].level != 1) {
fputs("lens-map v3 DP54 field round-trip regression failed\n", stderr); 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); unlink(dp_path); goto done;
@@ -1497,6 +1503,41 @@ int main(void) {
lens_map_destroy(&dloaded); 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); unlink(dp_path);
} }
/* Legacy v2 import: a real v2 map (no adaptive fields, no cost counters) /* Legacy v2 import: a real v2 map (no adaptive fields, no cost counters)
+94 -17
View File
@@ -7,26 +7,103 @@
static int nearly_equal(double a, double b) { return fabs(a - b) < 1e-12; } 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 /* Every enumerator must have a stable label, appended detail codes must be
* reported as UNKNOWN rather than indexing past a table. */ * 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) { static int check_reason_names(void) {
const RayReason reasons[] = { static const char *const frozen[] = {
RAY_REASON_NONE, RAY_REASON_REDSHIFT_LIMIT, "NONE", "REDSHIFT_LIMIT", "BUDGET_EXHAUSTED",
RAY_REASON_BUDGET_EXHAUSTED, RAY_REASON_TIME_RANGE_EXHAUSTED, "TIME_RANGE_EXHAUSTED", "OUT_OF_DOMAIN", "INVALID_METRIC",
RAY_REASON_OUT_OF_DOMAIN, RAY_REASON_INVALID_METRIC, "INTEGRATION_ERROR", "UNSUPPORTED", "PROTOCOL_ERROR",
RAY_REASON_INTEGRATION_ERROR, RAY_REASON_UNSUPPORTED, "IO_ERROR"};
RAY_REASON_PROTOCOL_ERROR, RAY_REASON_IO_ERROR};
int failed = 0; int failed = 0;
for (size_t i = 0; i < sizeof reasons / sizeof reasons[0]; ++i) { for (size_t i = 0; i < sizeof frozen / sizeof frozen[0]; ++i) {
const char *name = ray_reason_name(reasons[i]); const RayReason r = (RayReason)i;
if (name == NULL || name[0] == '\0' || strcmp(name, "UNKNOWN") == 0) { if (!ray_reason_valid(r) || strcmp(ray_reason_name(r), frozen[i]) != 0 ||
fprintf(stderr, "reason %d has no stable label\n", (int)reasons[i]); ray_reason_category(r) != r) {
fprintf(stderr, "frozen reason id %zu is not stable\n", i);
failed = 1; failed = 1;
} }
} }
const int unknown = (int)RAY_REASON_IO_ERROR + 7; if ((unsigned)RAY_REASON_IO_ERROR + 1u >= (unsigned)RAY_REASON_COUNT) {
if (strcmp(ray_reason_name((RayReason)unknown), "UNKNOWN") != 0) { fputs("no appended detail reasons\n", stderr);
fputs("out-of-range reason is not UNKNOWN\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; failed = 1;
} }
return failed; return failed;
@@ -35,8 +112,7 @@ static int check_reason_names(void) {
static int check_ray(const SpacetimeSource *source, static int check_ray(const SpacetimeSource *source,
const ObserverState *observer, const ObserverState *observer,
const double local_direction[3], const double local_direction[3],
const double expected[3]) { const double expected[3]) { const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
.max_steps = 100}; .max_steps = 100};
RayEndpoint ray = RayEndpoint ray =
geodesic_trace_past(source, observer, local_direction, &config); 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)) !spacetime_slab_eval(slab, 0.25, (double[]){0.0, 0.0, 0.0}, &metric))
return 1; return 1;
int result = check_reason_names() || int result = check_reason_names() ||
check_invalid_directions(&source, &observer) ||
check_ray(&source, &observer, (double[]){1.0, 0.0, 0.0}, check_ray(&source, &observer, (double[]){1.0, 0.0, 0.0},
(double[]){0.0, 0.0, -1.0}) || (double[]){0.0, 0.0, -1.0}) ||
check_ray(&source, &observer, (double[]){0.0, 0.0, 1.0}, check_ray(&source, &observer, (double[]){0.0, 0.0, 1.0},
+82 -14
View File
@@ -345,26 +345,26 @@ static void test_minkowski_finite_interval(void) {
RayEndpoint out = blank_endpoint(); RayEndpoint out = blank_endpoint();
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED && GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR, out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
"zero atol rejected"); "zero atol rejected");
bad = dp_config(1e-9, 0.5, 1.0e6); bad = dp_config(1e-9, 0.5, 1.0e6);
bad.max_lookback_time = 0.0; bad.max_lookback_time = 0.0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED && GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR, out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
"zero lookback rejected"); "zero lookback rejected");
bad = dp_config(1e-9, 0.5, 1.0e6); bad = dp_config(1e-9, 0.5, 1.0e6);
bad.min_step = 2.0; bad.min_step = 2.0;
bad.max_step = 1.0; bad.max_step = 1.0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED && GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR, out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
"inverted step bounds rejected"); "inverted step bounds rejected");
bad = dp_config(1e-9, 0.5, 1.0e6); bad = dp_config(1e-9, 0.5, 1.0e6);
bad.consecutive_rejection_limit = 0; bad.consecutive_rejection_limit = 0;
CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) == CHECK(geodesic_advance_past_ray(s, &bad_state, -0.5, &bad, &out) ==
GEODESIC_ADVANCE_FAILED && GEODESIC_ADVANCE_FAILED &&
out.reason == RAY_REASON_PROTOCOL_ERROR, out.reason == RAY_REASON_INVALID_STEPPER_CONFIG,
"zero reject limit rejected"); "zero reject limit rejected");
spacetime_free_slab(s); 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); geodesic_advance_past_ray(slab, &state, -0.5, &config, &out);
spacetime_free_slab(slab); spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED && 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"); "stage INTERNAL_ERROR is a direct protocol failure");
CHECK(state.rhs_evaluations == 2u, "internal error not retried"); 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); geodesic_advance_past_ray(slab, &state, -10.0, &hmin, &out);
spacetime_free_slab(slab); spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED && 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"); "step below hmin is an integration error");
CHECK(state.rejected_steps >= 1u, "hmin path rejected before failing"); 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( const RayEndpoint traced = geodesic_trace_past(
&source, &observer, (double[]){1.0, 0.0, 0.0}, &config); &source, &observer, (double[]){1.0, 0.0, 0.0}, &config);
CHECK(traced.outcome == RAY_OUTCOME_INCOMPLETE && CHECK(traced.outcome == RAY_OUTCOME_INCOMPLETE &&
traced.reason == RAY_REASON_PROTOCOL_ERROR, traced.reason == RAY_REASON_UNKNOWN_STEPPER,
"trace_past rejects an unknown stepper"); "trace_past rejects an unknown stepper");
GeodesicRayState state; GeodesicRayState state;
@@ -1794,7 +1794,7 @@ static void test_unknown_stepper_rejected(void) {
const RayEndpoint resumed = const RayEndpoint resumed =
geodesic_trace_past_from_state(&source, &state, &config); geodesic_trace_past_from_state(&source, &state, &config);
CHECK(resumed.outcome == RAY_OUTCOME_INCOMPLETE && 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"); "trace_past_from_state rejects an unknown stepper");
MetricSlab *slab = NULL; MetricSlab *slab = NULL;
@@ -1805,7 +1805,7 @@ static void test_unknown_stepper_rejected(void) {
spacetime_free_slab(slab); spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED && CHECK(r == GEODESIC_ADVANCE_FAILED &&
advanced.outcome == RAY_OUTCOME_INCOMPLETE && advanced.outcome == RAY_OUTCOME_INCOMPLETE &&
advanced.reason == RAY_REASON_PROTOCOL_ERROR, advanced.reason == RAY_REASON_UNKNOWN_STEPPER,
"advance rejects an unknown stepper"); "advance rejects an unknown stepper");
spacetime_destroy(&source); 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, fabs(null_residual(&metric, state.Pi) - 1.0) < 1e-12,
"time-precision fixture stays null"); "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 /* 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"); "boundary roundoff start escapes outward");
} }
/* A single-end state clearly outside its worldtube is a protocol error, not an /* A single-end state clearly outside its worldtube is reported with the exact
* unbounded search that can only end as budget exhaustion. */ * 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) { static void test_event_clearly_outside_protocol(void) {
FixtureContext c; FixtureContext c;
memset(&c, 0, sizeof c); memset(&c, 0, sizeof c);
@@ -2121,8 +2152,15 @@ static void test_event_clearly_outside_protocol(void) {
spacetime_free_slab(slab); spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED && CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE && out.outcome == RAY_OUTCOME_INCOMPLETE &&
out.reason == RAY_REASON_PROTOCOL_ERROR, out.reason == RAY_REASON_OUTSIDE_WORLDTUBE,
"clearly-outside single end is a protocol error"); "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 /* 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); spacetime_free_slab(slab);
CHECK(r == GEODESIC_ADVANCE_FAILED && CHECK(r == GEODESIC_ADVANCE_FAILED &&
out.outcome == RAY_OUTCOME_INCOMPLETE && 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"); "unconfirmable threshold is an incomplete integration failure");
CHECK(out.outcome != RAY_OUTCOME_DARK, CHECK(out.outcome != RAY_OUTCOME_DARK,
"unconfirmable threshold is not rewritten into 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"); 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 /* 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 * same flat escape must give event time/scale ~ 3 and x/scale ~ 5, i.e. no
* absolute coordinate-time floor breaks small units. */ * absolute coordinate-time floor breaks small units. */
@@ -2611,6 +2678,7 @@ int main(void) {
test_event_worldtube_polynomial_time_origin(); test_event_worldtube_polynomial_time_origin();
test_event_threshold_closed_endpoint(); test_event_threshold_closed_endpoint();
test_event_threshold_confirmation_exhausted(); test_event_threshold_confirmation_exhausted();
test_rejection_limit_reason();
test_event_unit_scaling(); test_event_unit_scaling();
if (failures != 0) { if (failures != 0) {
fprintf(stderr, "geodesic adaptive regression: %d failure(s)\n", failures); fprintf(stderr, "geodesic adaptive regression: %d failure(s)\n", failures);
+11 -9
View File
@@ -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: A deterministic DP54 integration error is produced without a large workload:
strict tolerances, ``min == initial == max`` step, and one allowed rejection strict tolerances, ``min == initial == max`` step, and one allowed rejection
make the first trial step fail as ``INTEGRATION_ERROR``. Small 16x8 scenes make the first trial step fail. The rejection quota is reached before any
keep every case short. Two-frame movie coverage uses the existing 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 ``test_observer_<backend>`` fixture track duplicated to two rows (the
Schwarzschild metric is stationary, so the same tetrad is valid at both 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 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, '--fov-deg', 80, '--exposure', 1e-3, '--coarse-cell-pixels', 8,
'--refine-max-level', 0, '--psf-relative-tail', 1e-4] '--refine-max-level', 0, '--psf-relative-tail', 1e-4]
# Deterministic DP54 integration failure: tight tolerances, a single fixed step # Deterministic DP54 rejection-quota failure: tight tolerances, a single fixed
# bound, and one allowed rejection. Every ray rejects its first trial step and # step bound, and one allowed rejection. Every ray rejects its first trial step
# reports INTEGRATION_ERROR instead of a fabricated terminal category. # and reports the exact REJECTION_LIMIT reason instead of a fabricated terminal
# category.
INJECT = ['--integrator', 'dp54', INJECT = ['--integrator', 'dp54',
'--ode-rtol', '1e-15', '--ode-atol-x', '1e-15', '--ode-rtol', '1e-15', '--ode-atol-x', '1e-15',
'--ode-atol-pi', '1e-15', '--ode-atol-l', '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) '--output', single)
err = single_run.stderr err = single_run.stderr
assert 'Ray failures:' in err, err 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 assert frame_reported(err, 0), err
if not is_debug(single_run): if not is_debug(single_run):
assert 'ray failure:' not in err, err assert 'ray failure:' not in err, err
@@ -154,7 +156,7 @@ with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
'--output', verbose) '--output', verbose)
err = verbose_run.stderr err = verbose_run.stderr
assert 'ray failure:' in err, err 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 'vertex=' in err and 'film=(' in err, err
assert 'accepted=' in err and 'rejected=' in err and 'rhs=' 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 # 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) '--output', tmp / f'map_refused.{ext}', ok=False)
err = refused.stderr err = refused.stderr
assert frame_reported(err, 0) and frame_reported(err, 1), err 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 'phase=import' in err, err
assert 'Incomplete render refused' in err, err assert 'Incomplete render refused' in err, err
assert err.index('frame 0') < err.index('Incomplete render refused'), 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) '--verbose', '--output', replay)
err = replay_run.stderr err = replay_run.stderr
assert 'Ray failures:' in err and frame_reported(err, 0), err 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 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 # 6) Budget exhaustion is a distinct, always-on message, and its refusal