Fix: Report ray failure reasons and affected frames

This commit is contained in:
wyj committed 2026-10-08 00:16:43 -04:00
1 parent af8b83007f
commit 76ed705d37
8 files changed
+1009 -37

No files matched your search

+1
View File
@@ -299,6 +299,7 @@ test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ADAPTIVE_GEODESIC_TEST_TARGET) $(A
$(SENSOR_BLOOM_TEST_TARGET)
python3 tests/test_camera_cli.py $(BUILD_DIR) $(TEST_OUT_DIR)
python3 tests/test_adaptive_cli.py $(BUILD_DIR) $(TEST_OUT_DIR)
python3 tests/test_ray_diagnostics.py $(BUILD_DIR) $(TEST_OUT_DIR)
python3 tests/test_output_streams.py $(BUILD_DIR) $(TEST_OUT_DIR)
tone-map-test: $(TONE_MAP_TEST_TARGET)
+11
View File
@@ -779,6 +779,17 @@ witness 提升为正式 midpoint 时原地复用同一 vertex id,只保留一
provenance 保存 coordinate-time step 与初始 step 预算,使实际积分来源与成本可
replay。
诊断输出所有权:所有 ray 失败报告只在 CLI/main 的串行后处理阶段发出,
ray-tracing 的 OpenMP worker 与 geodesic 热循环不写任何日志(既有 PSF splat
诊断不属于此范围)。稳定的 `ray_reason_name` 覆盖全部
`RayReason`;默认(非 verbose)也按 frame/phase 输出 `INCOMPLETE` 的 reason
直方图与真实 frame id、相机坐标时间,verbose 或 GR_DEBUG 才追加每 reason 有界
数量的代表性样本(generation/phase、sample id/kind、持久 vertex id、film 坐标、
可信 endpoint 的 stop 状态与 accepted/rejected/RHS 计数)并报告被抑制数量。
`UNRESOLVED/BUDGET_EXHAUSTED` 只在帧边界统计确认存在 blocking triangle 时按帧
报告,与数值 `INCOMPLETE` 区分;replay 使用文件中保存的几何与顶点诊断,不虚构
stop 状态,也不把未重试的旧失败当作新失败重复计数。
---
# 18A. 渐近外区、escape worldtube 与 endpoint 协议
+26
View File
@@ -9,6 +9,32 @@ typedef struct {
double x[3], Pi[3], log_alpha_p0;
} Derivative;
const char *ray_reason_name(RayReason reason) {
switch (reason) {
case RAY_REASON_NONE:
return "NONE";
case RAY_REASON_REDSHIFT_LIMIT:
return "REDSHIFT_LIMIT";
case RAY_REASON_BUDGET_EXHAUSTED:
return "BUDGET_EXHAUSTED";
case RAY_REASON_TIME_RANGE_EXHAUSTED:
return "TIME_RANGE_EXHAUSTED";
case RAY_REASON_OUT_OF_DOMAIN:
return "OUT_OF_DOMAIN";
case RAY_REASON_INVALID_METRIC:
return "INVALID_METRIC";
case RAY_REASON_INTEGRATION_ERROR:
return "INTEGRATION_ERROR";
case RAY_REASON_UNSUPPORTED:
return "UNSUPPORTED";
case RAY_REASON_PROTOCOL_ERROR:
return "PROTOCOL_ERROR";
case RAY_REASON_IO_ERROR:
return "IO_ERROR";
}
return "UNKNOWN";
}
static double dot(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
+4
View File
@@ -28,6 +28,10 @@ typedef enum {
RAY_REASON_IO_ERROR
} RayReason;
/* 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. */
const char *ray_reason_name(RayReason reason);
/* 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
* function of position and must not be confused with the true infinity
+641 -36
View File
@@ -961,23 +961,463 @@ static void ray_pool_status_counts(const RayPool *rays, size_t *pending,
}
}
/* ------------------------------------------------------------------------- *
* Serial ray-failure diagnostics.
*
* Ray-failure reporting runs outside the ray-tracing workers and at the CLI
* only; it never logs from an OpenMP tracing loop. Bulk tracing results are
* inspected once it has returned. A compact per-frame histogram of INCOMPLETE
* endpoints is always written to stderr, even without --verbose. --verbose or
* a GR_DEBUG build adds a bounded number of representative samples with
* film/cost localization. Nothing here changes a physics outcome, a
* termination category, or a retry rule; it only makes an already-computed
* result visible with its reason and frame.
* ------------------------------------------------------------------------- */
#define RAY_DIAG_REASON_UNKNOWN_INDEX ((size_t)RAY_REASON_IO_ERROR + 1u)
#define RAY_DIAG_REASON_COUNT (RAY_DIAG_REASON_UNKNOWN_INDEX + 1u)
#define RAY_DIAG_REPS_PER_REASON 3u
static const char *frame_sample_kind_name(FrameSampleKind kind) {
switch (kind) {
case FRAME_SAMPLE_VERTEX:
return "vertex";
case FRAME_SAMPLE_PROBE:
return "probe";
case FRAME_SAMPLE_RETRY:
return "retry";
}
return "unknown";
}
/* Known reasons keep their own histogram bucket; an out-of-range value gets a
* dedicated UNKNOWN bucket instead of being folded into NONE. */
static size_t raydiag_reason_index(RayReason reason) {
const size_t index = (size_t)reason;
return index < RAY_DIAG_REASON_UNKNOWN_INDEX ? index
: RAY_DIAG_REASON_UNKNOWN_INDEX;
}
static const char *raydiag_reason_label(size_t index) {
return index == RAY_DIAG_REASON_UNKNOWN_INDEX
? "UNKNOWN"
: ray_reason_name((RayReason)index);
}
/* Representative detail is requested by --verbose or a GR_DEBUG build; the
* compact histogram is emitted regardless. */
static int raydiag_wants_samples(const Settings *settings) {
#ifdef GR_DEBUG
(void)settings;
return 1;
#else
return settings != NULL && settings->verbose;
#endif
}
/* One bounded, honest descriptor of a failed ray request. Every optional
* field carries a `has_*` flag so a report never invents a trusted stop state,
* a persistent vertex id, or a film position the source did not retain. */
typedef struct {
size_t reason;
int has_sample;
size_t sample_id;
int has_kind;
FrameSampleKind kind;
int has_probe;
int probe;
int has_persistent;
size_t persistent_id;
int has_film;
double film_x, film_y;
int trusted;
double stop_time;
int has_position;
double position[3];
unsigned long long accepted;
unsigned long long rejected;
unsigned long long rhs;
} RayDiagSample;
typedef struct {
const Settings *settings;
size_t counts[RAY_DIAG_REASON_COUNT];
size_t total;
size_t rep_count[RAY_DIAG_REASON_COUNT];
RayDiagSample reps[RAY_DIAG_REASON_COUNT][RAY_DIAG_REPS_PER_REASON];
size_t suppressed[RAY_DIAG_REASON_COUNT];
} RayDiagReport;
static void raydiag_report_init(RayDiagReport *report,
const Settings *settings) {
memset(report, 0, sizeof *report);
report->settings = settings;
}
static void raydiag_report_add(RayDiagReport *report, RayReason reason,
const RayDiagSample *sample) {
const size_t index = raydiag_reason_index(reason);
++report->counts[index];
++report->total;
if (!raydiag_wants_samples(report->settings))
return;
if (report->rep_count[index] < RAY_DIAG_REPS_PER_REASON) {
report->reps[index][report->rep_count[index]] = *sample;
report->reps[index][report->rep_count[index]].reason = (size_t)reason;
++report->rep_count[index];
} else {
++report->suppressed[index];
}
}
/* `generation`/`slab` are negative when the phase has no meaningful value. */
static void raydiag_report_flush(const RayDiagReport *report, size_t frame_id,
double camera_time, const char *phase,
long long generation, long long slab) {
if (report->total == 0)
return;
fprintf(stderr, "Ray failures: frame %zu camera_t=%.9g phase=%s", frame_id,
camera_time, phase);
if (generation >= 0)
fprintf(stderr, " generation=%lld", generation);
if (slab >= 0)
fprintf(stderr, " slab=%lld", slab);
fprintf(stderr, ": INCOMPLETE=%zu [", report->total);
int first = 1;
for (size_t i = 0; i < RAY_DIAG_REASON_COUNT; ++i) {
if (report->counts[i] == 0)
continue;
fprintf(stderr, "%s%s=%zu", first ? "" : " ", raydiag_reason_label(i),
report->counts[i]);
first = 0;
}
fputs("]\n", stderr);
if (!raydiag_wants_samples(report->settings))
return;
for (size_t i = 0; i < RAY_DIAG_REASON_COUNT; ++i) {
for (size_t r = 0; r < report->rep_count[i]; ++r) {
const RayDiagSample *d = &report->reps[i][r];
fprintf(stderr, " ray failure: frame=%zu reason=%s", frame_id,
ray_reason_name((RayReason)d->reason));
if (d->has_sample)
fprintf(stderr, " sample=%zu", d->sample_id);
if (d->has_kind)
fprintf(stderr, " kind=%s", frame_sample_kind_name(d->kind));
if (d->has_probe)
fprintf(stderr, " probe=%d", d->probe);
if (d->has_persistent)
fprintf(stderr, " vertex=%zu", d->persistent_id);
if (d->has_film)
fprintf(stderr, " film=(%.6g, %.6g)", d->film_x, d->film_y);
fprintf(stderr, " trusted=%d", d->trusted);
if (d->trusted) {
fprintf(stderr, " stop_t=%.9g", d->stop_time);
if (d->has_position)
fprintf(stderr, " pos=(%.9g, %.9g, %.9g)", d->position[0],
d->position[1], d->position[2]);
}
fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n", d->accepted,
d->rejected, d->rhs);
}
if (report->suppressed[i] != 0)
fprintf(stderr, " ray failure: frame=%zu reason=%s suppressed=%zu\n",
frame_id, raydiag_reason_label(i), report->suppressed[i]);
}
}
/* Growing byte map keyed by persistent vertex id, used to avoid re-reporting a
* failure that a later refinement generation still contains. */
typedef struct {
unsigned char *bytes;
size_t capacity;
} RayDiagSeen;
static int raydiag_seen_reserve(RayDiagSeen *seen, size_t count) {
if (count <= seen->capacity)
return 0;
size_t capacity = seen->capacity != 0 ? seen->capacity : 64;
while (capacity < count) {
if (capacity > SIZE_MAX / 2) {
capacity = count;
break;
}
capacity *= 2;
}
unsigned char *bytes = realloc(seen->bytes, capacity);
if (bytes == NULL)
return -1;
memset(bytes + seen->capacity, 0, capacity - seen->capacity);
seen->bytes = bytes;
seen->capacity = capacity;
return 0;
}
static void raydiag_seen_destroy(RayDiagSeen *seen) {
free(seen->bytes);
seen->bytes = NULL;
seen->capacity = 0;
}
/* Emit INCOMPLETE diagnostics for a finalized mesh. When `seen` is non-NULL
* every reported vertex is marked, so a refinement sequence never repeats an
* already-surfaced failure; a NULL `seen` reports every INCOMPLETE vertex (the
* one-shot imported-map scan). Vertices retain no trusted stop state, so the
* representative detail intentionally omits stop time/position. Returns 0 on
* success and -1 only when growing the seen set fails. */
static int report_mesh_incomplete(const Settings *s, const FrameLensMesh *mesh,
RayDiagSeen *seen, size_t frame_id,
double camera_time, const char *phase,
long long generation, long long slab) {
if (mesh == NULL)
return 0;
if (seen != NULL && raydiag_seen_reserve(seen, mesh->vertex_count))
return -1;
RayDiagReport report;
raydiag_report_init(&report, s);
for (size_t i = 0; i < mesh->vertex_count; ++i) {
const LensVertex *v = &mesh->vertices[i];
if (!v->traced || v->outcome != RAY_OUTCOME_INCOMPLETE)
continue;
if (seen != NULL) {
if (seen->bytes[i])
continue;
seen->bytes[i] = 1;
}
RayDiagSample sample = {0};
sample.has_persistent = 1;
sample.persistent_id = i;
sample.has_probe = 1;
sample.probe = v->diagnostic_probe != 0;
sample.has_film = isfinite(v->image_x) && isfinite(v->image_y);
sample.film_x = v->image_x;
sample.film_y = v->image_y;
sample.accepted = v->trace_accepted_steps;
sample.rejected = v->trace_rejected_steps;
sample.rhs = v->trace_rhs_evaluations;
raydiag_report_add(&report, v->reason, &sample);
}
raydiag_report_flush(&report, frame_id, camera_time, phase, generation, slab);
return 0;
}
/* Inspect this generation's pool for terminal INCOMPLETE endpoints not yet
* reported. Slots are appended grouped by frame, so one pass emits one
* summary per affected frame; `reported` is a per-slot byte map owned by the
* caller so later slabs never re-report an earlier failure. `phase` records
* whether the failure was found during preroute or a slab sweep. */
static void report_pool_incomplete(const Settings *s, const Movie *movie,
const RayPool *rays, unsigned char *reported,
size_t generation, long long slab_id,
const char *phase) {
if (rays == NULL || reported == NULL || rays->count == 0)
return;
RayDiagReport report;
raydiag_report_init(&report, s);
long long frame_index = -1;
size_t frame_id = 0;
double camera_time = 0.0;
for (size_t i = 0; i < rays->count; ++i) {
if (reported[i])
continue;
const RayPoolStatus status = rays->status[i];
/* PENDING/ACTIVE slots still hold their zero-initialized INCOMPLETE
* placeholder; only a terminal status carries a real failure reason. */
if (status != RAY_POOL_TERMINATED && status != RAY_POOL_FAILED)
continue;
const RayEndpoint *endpoint = &rays->endpoint[i];
if (endpoint->outcome != RAY_OUTCOME_INCOMPLETE)
continue;
reported[i] = 1;
const long long frame = (long long)rays->frame_id[i];
if (frame != frame_index) {
if (frame_index >= 0)
raydiag_report_flush(&report, frame_id, camera_time, phase,
(long long)generation, slab_id);
raydiag_report_init(&report, s);
frame_index = frame;
if (movie != NULL && frame >= 0 && (size_t)frame < movie->frame_count) {
frame_id = movie->frames[frame].frame_id;
camera_time = movie->frames[frame].coordinate_time;
} else {
frame_id = rays->frame_id[i];
camera_time = NAN;
}
}
RayDiagSample sample = {0};
sample.has_sample = 1;
sample.sample_id = rays->vertex_id[i];
sample.has_film = 0;
if (movie != NULL && frame >= 0 && (size_t)frame < movie->frame_count) {
const FrameLensMesh *mesh = &movie->frames[frame].mesh;
size_t sample_count = 0;
const FrameSample *samples =
frame_lens_mesh_samples(mesh, &sample_count);
if (samples != NULL && sample.sample_id < sample_count) {
const FrameSample *fs = &samples[sample.sample_id];
sample.has_kind = 1;
sample.kind = fs->kind;
sample.has_probe = 1;
/* Fresh probes have not become persistent witnesses yet; retries
* retain the witness flag even though their request kind is RETRY. */
sample.probe = fs->kind == FRAME_SAMPLE_PROBE ||
fs->vertex.diagnostic_probe != 0;
sample.has_film =
isfinite(fs->vertex.image_x) && isfinite(fs->vertex.image_y);
sample.film_x = fs->vertex.image_x;
sample.film_y = fs->vertex.image_y;
/* A vertex and a retry reference a persistent mesh vertex directly; a
* cached probe is an existing witness. A fresh probe has no persistent
* id yet and is left unset. */
const int persistent_kind =
fs->kind == FRAME_SAMPLE_VERTEX || fs->kind == FRAME_SAMPLE_RETRY ||
(fs->kind == FRAME_SAMPLE_PROBE && fs->cached);
if (persistent_kind && fs->vertex_id < mesh->vertex_count) {
sample.has_persistent = 1;
sample.persistent_id = fs->vertex_id;
}
}
}
sample.has_position = 1;
for (int axis = 0; axis < 3; ++axis)
sample.position[axis] = endpoint->final_x[axis];
sample.trusted = isfinite(endpoint->stop_coordinate_time) &&
isfinite(endpoint->final_x[0]) &&
isfinite(endpoint->final_x[1]) &&
isfinite(endpoint->final_x[2]);
sample.stop_time = endpoint->stop_coordinate_time;
sample.accepted = endpoint->accepted_steps;
sample.rejected = endpoint->rejected_steps;
sample.rhs = endpoint->rhs_evaluations;
raydiag_report_add(&report, endpoint->reason, &sample);
}
if (frame_index >= 0)
raydiag_report_flush(&report, frame_id, camera_time, phase,
(long long)generation, slab_id);
}
/* Probes that failed but were not persisted as a mesh vertex (finish_generation
* may not have run after an error). Cached probes are deliberately skipped:
* they are pre-existing samples, not new failures of this generation. */
static void report_pending_probes(const Settings *s, const FrameLensMesh *mesh,
size_t frame_id, double camera_time) {
if (mesh == NULL || mesh->samples == NULL)
return;
RayDiagReport report;
raydiag_report_init(&report, s);
for (size_t i = 0; i < mesh->sample_count; ++i) {
const FrameSample *fs = &mesh->samples[i];
if (fs->cached || fs->kind != FRAME_SAMPLE_PROBE ||
!fs->vertex.traced || fs->vertex.outcome != RAY_OUTCOME_INCOMPLETE)
continue;
RayDiagSample sample = {0};
sample.has_sample = 1;
sample.sample_id = i;
sample.has_kind = 1;
sample.kind = FRAME_SAMPLE_PROBE;
sample.has_probe = 1;
sample.probe = 1;
sample.has_film =
isfinite(fs->vertex.image_x) && isfinite(fs->vertex.image_y);
sample.film_x = fs->vertex.image_x;
sample.film_y = fs->vertex.image_y;
sample.accepted = fs->vertex.trace_accepted_steps;
sample.rejected = fs->vertex.trace_rejected_steps;
sample.rhs = fs->vertex.trace_rhs_evaluations;
raydiag_report_add(&report, fs->vertex.reason, &sample);
}
raydiag_report_flush(&report, frame_id, camera_time, "refinement-error", -1,
-1);
}
/* Per-frame UNRESOLVED/BUDGET_EXHAUSTED summary from finalized boundary stats.
* This is a resource quota result, not a numerical failure: it is emitted even
* without --verbose so a refused frame is never silent, while verbose/Debug
* adds bounded sample detail. Normal DARK and approximate-black triangles are
* not diagnosed here. `continuation_valid` is false for an imported map,
* whose wire format does not persist the resume state: replay must not present
* a zero-initialized continuation time as if it were observed. */
static void report_unresolved_frame(const Settings *s,
const FrameLensMesh *mesh, size_t frame_id,
double camera_time,
const FrameBoundaryStats *stats,
int continuation_valid) {
if (mesh == NULL || stats == NULL || stats->budget_incomplete_triangles == 0)
return;
size_t unresolved_vertices = 0;
for (size_t i = 0; i < mesh->vertex_count; ++i)
if (mesh->vertices[i].traced &&
mesh->vertices[i].outcome == RAY_OUTCOME_UNRESOLVED)
++unresolved_vertices;
fprintf(stderr,
"UNRESOLVED/BUDGET_EXHAUSTED: frame %zu camera_t=%.9g "
"blocking_triangles=%zu unresolved_samples=%zu\n",
frame_id, camera_time, stats->budget_incomplete_triangles,
unresolved_vertices);
if (!raydiag_wants_samples(s))
return;
size_t shown = 0;
for (size_t i = 0;
i < mesh->vertex_count && shown < RAY_DIAG_REPS_PER_REASON; ++i) {
const LensVertex *v = &mesh->vertices[i];
if (!v->traced || v->outcome != RAY_OUTCOME_UNRESOLVED)
continue;
fprintf(stderr,
" unresolved sample: frame=%zu vertex=%zu film=(%.6g, %.6g) "
"continuation_t=",
frame_id, i, v->image_x, v->image_y);
if (continuation_valid)
fprintf(stderr, "%.9g", v->continuation_t);
else
fputs("-", stderr);
fprintf(stderr, " accepted=%llu rejected=%llu rhs=%llu\n",
(unsigned long long)v->trace_accepted_steps,
(unsigned long long)v->trace_rejected_steps,
(unsigned long long)v->trace_rhs_evaluations);
++shown;
}
}
typedef struct {
const Settings *settings;
const FrameLensMesh *mesh;
size_t frame_id;
double camera_time;
RayDiagSeen seen;
int alloc_failed;
} RefinementDiagnostics;
/* Always installed as the refinement progress callback: normal progress text
* stays gated by --verbose, while completed generations always run the
* bounded INCOMPLETE scan. A growth failure cannot be returned through the
* void callback, so it is recorded in the context and surfaced by the caller. */
static void report_frame_refinement(void *context, size_t generation,
size_t sample_count, size_t vertex_count,
size_t triangle_count, int added_vertices,
int finished) {
const Settings *settings = context;
if (settings == NULL || !settings->verbose)
RefinementDiagnostics *diag = context;
if (diag == NULL)
return;
if (!finished)
fprintf(stdout,
"Frame 0: refinement generation %zu tracing %zu samples "
"from %zu vertices and %zu triangles.\n",
generation, sample_count, vertex_count, triangle_count);
else
fprintf(stdout,
"Frame 0: refinement generation %zu finished; added %d vertices, "
"now %zu vertices and %zu triangles.\n",
generation, added_vertices, vertex_count, triangle_count);
const Settings *settings = diag->settings;
if (settings != NULL && settings->verbose) {
if (!finished)
fprintf(stdout,
"Frame %zu: refinement generation %zu tracing %zu samples "
"from %zu vertices and %zu triangles.\n",
diag->frame_id, generation, sample_count, vertex_count,
triangle_count);
else
fprintf(stdout,
"Frame %zu: refinement generation %zu finished; added %d "
"vertices, now %zu vertices and %zu triangles.\n",
diag->frame_id, generation, added_vertices, vertex_count,
triangle_count);
}
if (!finished || diag->alloc_failed)
return;
if (report_mesh_incomplete(settings, diag->mesh, &diag->seen, diag->frame_id,
diag->camera_time, "refinement",
(long long)generation, -1))
diag->alloc_failed = 1;
}
#ifdef SPACETIME_ALCUBIERRE
@@ -1060,9 +1500,13 @@ static double alcubierre_step_budget(const Settings *s) {
/* Recompute per-triangle approximate-black provenance and aggregate the E/D/U
* accounting across the given frames. The caller uses the returned totals both
* for the verbose report and for boundary_allows_publish() production gating. */
* for the verbose report and for boundary_allows_publish() production gating.
* `frame_ids`/`camera_times` label the always-on per-frame UNRESOLVED summary
* with the real frame id and camera coordinate time. */
static void report_boundary_stats(const Settings *s,
FrameLensMesh *const *meshes,
const size_t *frame_ids,
const double *camera_times,
size_t frame_count,
const RefinementConfig *config,
FrameBoundaryStats *out_total) {
@@ -1070,6 +1514,11 @@ static void report_boundary_stats(const Settings *s,
for (size_t i = 0; i < frame_count; ++i) {
FrameBoundaryStats frame_stats;
frame_lens_mesh_boundary_stats(meshes[i], config, &frame_stats);
/* Emitted before the publication gate so a budget-incomplete frame is
* never silent even when --verbose is off. These meshes come from live
* tracing, so the UNRESOLVED continuation state is real. */
report_unresolved_frame(s, meshes[i], frame_ids[i], camera_times[i],
&frame_stats, 1);
total.escaped_only += frame_stats.escaped_only;
total.dark_only += frame_stats.dark_only;
total.eed_edd += frame_stats.eed_edd;
@@ -1178,7 +1627,9 @@ static LensMapProvenance lens_map_provenance(const Settings *s,
/* Refuse to publish silently on true errors or on unresolved triangles that
* exhausted the configured total budget. Approximate-black UUD/UDD boundary
* triangles are an accepted finite-resolution error and do not block output.
* A diagnostic run may override this, but the incompleteness is reported. */
* A diagnostic run may override this, but the incompleteness is reported.
* The refusal distinguishes a numerical/domain error -- which a larger retry
* budget cannot repair -- from a pure budget shortage. */
static int boundary_allows_publish(const Settings *s,
const FrameBoundaryStats *total) {
const size_t blocking = total->error + total->budget_incomplete_triangles;
@@ -1191,11 +1642,28 @@ static int boundary_allows_publish(const Settings *s,
total->error, total->budget_incomplete_triangles);
return 1;
}
fprintf(stderr,
"Incomplete render refused: %zu error triangle(s), %zu "
"budget-incomplete unresolved triangle(s). Raise the retry budget or "
"pass --allow-incomplete for a diagnostic output.\n",
total->error, total->budget_incomplete_triangles);
if (total->error != 0 && total->budget_incomplete_triangles != 0) {
fprintf(stderr,
"Incomplete render refused: %zu error triangle(s) and %zu "
"budget-incomplete unresolved triangle(s). The error triangles are "
"numerical/domain failures that a larger retry budget does not fix; "
"raise the retry budget only for the unresolved triangles, or pass "
"--allow-incomplete for a diagnostic output.\n",
total->error, total->budget_incomplete_triangles);
} else if (total->error != 0) {
fprintf(stderr,
"Incomplete render refused: %zu error triangle(s). These are "
"numerical/domain/integration failures, not a budget shortage, so a "
"larger retry budget does not fix them; pass --allow-incomplete for "
"a diagnostic output.\n",
total->error);
} else {
fprintf(stderr,
"Incomplete render refused: %zu budget-incomplete unresolved "
"triangle(s). Raise the retry budget or pass --allow-incomplete for "
"a diagnostic output.\n",
total->budget_incomplete_triangles);
}
return 0;
}
@@ -1517,15 +1985,23 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
if (hdr == NULL || frame_lens_mesh_build_coarse(&mesh, s->width, s->height,
s->coarse_cell_pixels,
s->horizontal_fov_deg)) {
fputs("Frame 0: initial HDR allocation or coarse lens mesh build failed.\n",
stderr);
frame_lens_mesh_destroy(&mesh);
free(hdr);
return -1;
}
RefinementDiagnostics diag = {.settings = s,
.mesh = &mesh,
.frame_id = 0,
.camera_time = observer->coordinate_time};
if (s->verbose)
fprintf(stdout, "Frame 0: tracing %zu initial rays from %zu mesh triangles...\n",
mesh.vertex_count, mesh.triangle_count);
const double initial_trace_start = omp_get_wtime();
if (frame_lens_mesh_trace(&mesh, spacetime, observer, &trace)) {
fputs("Frame 0: initial ray trace failed.\n", stderr);
raydiag_seen_destroy(&diag.seen);
frame_lens_mesh_destroy(&mesh);
free(hdr);
return -1;
@@ -1533,17 +2009,45 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
if (s->verbose)
fprintf(stdout, "Frame 0: initial ray trace finished in %.3f s.\n",
omp_get_wtime() - initial_trace_start);
/* The initial vertices had no prior report, so this first scan establishes
* the seen set the refinement callback extends across generations. */
if (report_mesh_incomplete(s, &mesh, &diag.seen, 0, observer->coordinate_time,
"initial", -1, -1)) {
fputs("Frame 0: failed to allocate ray-failure diagnostics.\n", stderr);
raydiag_seen_destroy(&diag.seen);
frame_lens_mesh_destroy(&mesh);
free(hdr);
return -1;
}
if (s->refinement.max_level > 0 && s->verbose)
fprintf(stdout, "Frame 0: starting adaptive ray-trace refinement (max level %u)...\n",
s->refinement.max_level);
const double refinement_start = omp_get_wtime();
if (frame_lens_mesh_refine_with_progress(
&mesh, spacetime, observer, &trace, &refinement_config,
s->verbose ? report_frame_refinement : NULL, (void *)s)) {
report_frame_refinement, &diag)) {
/* A failed finish_generation may not persist its pending probes, so surface
* both the mesh and the still-pending samples before destruction. */
(void)report_mesh_incomplete(s, &mesh, &diag.seen, 0,
observer->coordinate_time, "refinement-error",
-1, -1);
report_pending_probes(s, &mesh, 0, observer->coordinate_time);
fputs("Frame 0: adaptive ray-trace refinement failed.\n", stderr);
raydiag_seen_destroy(&diag.seen);
frame_lens_mesh_destroy(&mesh);
free(hdr);
return -1;
}
if (diag.alloc_failed) {
fputs("Frame 0: failed to allocate ray-failure diagnostics.\n", stderr);
raydiag_seen_destroy(&diag.seen);
frame_lens_mesh_destroy(&mesh);
free(hdr);
return -1;
}
/* Diagnostics no longer need the vertex-provenance set once refinement has
* finished; release it before the render stage. */
raydiag_seen_destroy(&diag.seen);
if (s->refinement.max_level > 0 && s->verbose)
fprintf(stdout,
"Frame 0: adaptive ray-trace refinement finished in %.3f s; "
@@ -1552,8 +2056,10 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
mesh.triangle_count);
FrameBoundaryStats boundary_totals;
FrameLensMesh *frame_meshes[1] = {&mesh};
report_boundary_stats(s, frame_meshes, 1, &refinement_config,
&boundary_totals);
const size_t frame_ids[1] = {0};
const double camera_times[1] = {observer->coordinate_time};
report_boundary_stats(s, frame_meshes, frame_ids, camera_times, 1,
&refinement_config, &boundary_totals);
{
FrameTraceStats trace_stats;
frame_lens_mesh_trace_stats(&mesh, &trace_stats);
@@ -1650,7 +2156,13 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
for (size_t f = 0; f < movie->frame_count; ++f) {
const int prepared = frame_lens_mesh_prepare_generation(
&movie->frames[f].mesh, &effective);
if (prepared < 0) return -1;
if (prepared < 0) {
fprintf(stderr,
"Ray trace generation %zu: frame %zu could not prepare its next "
"sample generation.\n",
generation, movie->frames[f].frame_id);
return -1;
}
ray_count += (size_t)prepared;
}
if (ray_count == 0) return 0;
@@ -1658,7 +2170,25 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
fprintf(stdout,
"Ray trace generation %zu: collected %zu new samples across %zu frames.\n",
generation, ray_count, movie->frame_count);
if (ray_pool_init(&rays, ray_count)) return -1;
if (ray_pool_init(&rays, ray_count)) {
fprintf(stderr,
"Ray trace generation %zu: ray pool allocation failed for %zu "
"samples.\n",
generation, ray_count);
return -1;
}
/* Per-slot failure census, owned by this generation and freed on every exit
* alongside the pool. It lets each slab report only the samples that newly
* failed instead of re-counting the whole run every sweep. */
unsigned char *reported = calloc(ray_count, 1);
if (reported == NULL) {
fprintf(stderr,
"Ray trace generation %zu: failure-tracking allocation failed for "
"%zu samples.\n",
generation, ray_count);
ray_pool_destroy(&rays);
return -1;
}
for (size_t f = 0; f < movie->frame_count; ++f) {
size_t count = 0;
const FrameSample *samples = frame_lens_mesh_samples(&movie->frames[f].mesh, &count);
@@ -1669,6 +2199,11 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
if (fs->kind == FRAME_SAMPLE_RETRY) {
GeodesicRayState state;
if (frame_vertex_continuation_state(&fs->vertex, &state)) {
fprintf(stderr,
"Ray trace generation %zu: frame %zu sample %zu retry is "
"missing a valid continuation payload.\n",
generation, movie->frames[f].frame_id, sample);
free(reported);
ray_pool_destroy(&rays);
return -1;
}
@@ -1680,15 +2215,25 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
fs->vertex.camera_direction, f, sample);
}
if (rc) {
fprintf(stderr,
"Ray trace generation %zu: frame %zu sample %zu could not be "
"appended to the ray pool.\n",
generation, movie->frames[f].frame_id, sample);
free(reported);
ray_pool_destroy(&rays);
return -1;
}
}
}
ray_pool_preroute(&rays, spacetime);
/* Preroute may already terminate a ray as INCOMPLETE (unsupported chart,
* exhausted data time, protocol error). Report those before the sweep; the
* zero-initialized stop state is not trusted, so no stop details appear. */
report_pool_incomplete(s, movie, &rays, reported, generation, -1, "preroute");
double slab_hi = movie->frames[movie->frame_count - 1].coordinate_time;
size_t slab_id = 0;
while (ray_pool_has_live(&rays)) {
const size_t current_slab = ++slab_id;
const double slab_lo = slab_hi - s->slab_duration;
MetricSlab *slab = NULL;
size_t pending_before, active_before, terminated_before, unresolved_before,
@@ -1697,6 +2242,13 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
&terminated_before, &unresolved_before,
&failed_before);
if (spacetime_load_slab(spacetime, slab_hi, slab_lo, &slab)) {
/* No RayReason exists for a backend with no slab data; state the
* infrastructure failure and the time range instead of fabricating one. */
fprintf(stderr,
"Ray trace generation %zu: metric slab load failed for time range "
"[%.9g, %.9g].\n",
generation, slab_hi, slab_lo);
free(reported);
ray_pool_destroy(&rays);
return -1;
}
@@ -1708,6 +2260,9 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
&failed_active);
ray_pool_advance_active(&rays, slab, trace);
spacetime_free_slab(slab);
/* Serial, post-sweep reporting: never from an OpenMP worker. */
report_pool_incomplete(s, movie, &rays, reported, generation,
(long long)current_slab, "slab");
size_t pending_after, active_after, terminated_after, unresolved_after,
failed_after;
ray_pool_status_counts(&rays, &pending_after, &active_after,
@@ -1716,7 +2271,7 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
fprintf(stdout,
"Ray trace generation %zu, slab %zu [%.6g, %.6g]: activated %zu; "
"live %zu -> %zu, terminated %zu, unresolved %zu, failed %zu.\n",
generation, ++slab_id, slab_hi, slab_lo,
generation, current_slab, slab_hi, slab_lo,
active_active - active_before, pending_before + active_before,
pending_after + active_after, terminated_after, unresolved_after,
failed_after);
@@ -1725,9 +2280,16 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
for (size_t i = 0; i < rays.count; ++i)
if (frame_lens_mesh_install_sample(&movie->frames[rays.frame_id[i]].mesh,
rays.vertex_id[i], &rays.endpoint[i])) {
fprintf(stderr,
"Ray trace generation %zu: frame %zu sample %zu endpoint install "
"failed.\n",
generation, movie->frames[rays.frame_id[i]].frame_id,
rays.vertex_id[i]);
free(reported);
ray_pool_destroy(&rays);
return -1;
}
free(reported);
ray_pool_destroy(&rays);
if (s->verbose)
fprintf(stdout, "Ray trace generation %zu: installing endpoints and refining meshes.\n",
@@ -1739,7 +2301,9 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
const int added = frame_lens_mesh_finish_generation(&movie->frames[f].mesh,
&effective);
if (added < 0) {
fprintf(stderr, "Ray trace generation %zu: frame %zu refinement failed.\n",
fprintf(stderr,
"Ray trace generation %zu: frame %zu refinement failed while "
"applying the completed generation.\n",
generation, movie->frames[f].frame_id);
return -1;
}
@@ -1898,18 +2462,29 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
{
const RefinementConfig boundary_config = effective_refinement(s, &trace);
FrameLensMesh **meshes = malloc(movie.frame_count * sizeof *meshes);
if (meshes == NULL)
size_t *frame_ids = malloc(movie.frame_count * sizeof *frame_ids);
double *camera_times = malloc(movie.frame_count * sizeof *camera_times);
if (meshes == NULL || frame_ids == NULL || camera_times == NULL) {
free(meshes);
free(frame_ids);
free(camera_times);
goto done;
for (size_t i = 0; i < movie.frame_count; ++i)
}
for (size_t i = 0; i < movie.frame_count; ++i) {
meshes[i] = &movie.frames[i].mesh;
frame_ids[i] = movie.frames[i].frame_id;
camera_times[i] = movie.frames[i].coordinate_time;
}
FrameBoundaryStats boundary_totals;
report_boundary_stats(s, meshes, movie.frame_count, &boundary_config,
&boundary_totals);
report_boundary_stats(s, meshes, frame_ids, camera_times, movie.frame_count,
&boundary_config, &boundary_totals);
FrameTraceStats trace_totals = {0};
for (size_t i = 0; i < movie.frame_count; ++i)
accumulate_trace_stats(&movie.frames[i].mesh, &trace_totals);
report_trace_stats(s, &trace_totals, "Movie");
free(meshes);
free(frame_ids);
free(camera_times);
if (!boundary_allows_publish(s, &boundary_totals))
goto done;
}
@@ -2079,15 +2654,45 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
.retry_lookback_increment = map.provenance.retry_lookback_increment,
.max_total_lookback_time = map.provenance.max_total_lookback_time};
int replay_incomplete = 0;
/* Replay consumes stored decisions, not current CLI refinement defaults. */
/* Replay consumes stored decisions, not current CLI refinement defaults.
* Boundary stats and diagnostics are computed for every frame first, so the
* publication gate can never hide a later frame's reported reason. */
FrameBoundaryStats *replay_stats =
calloc(map.frame_count, sizeof *replay_stats);
if (replay_stats == NULL) {
fputs("Imported lens map: failed to allocate per-frame diagnostic state.\n",
stderr);
lens_map_destroy(&map);
return -1;
}
for (size_t f = 0; f < map.frame_count; ++f)
frame_lens_mesh_boundary_stats(&map.frames[f].mesh, &replay_config,
&replay_stats[f]);
for (size_t f = 0; f < map.frame_count; ++f) {
FrameBoundaryStats completion;
frame_lens_mesh_boundary_stats(&map.frames[f].mesh, &replay_config, &completion);
replay_incomplete |= completion.error != 0 || completion.budget_incomplete_triangles != 0;
if (!boundary_allows_publish(s, &completion)) {
lens_map_destroy(&map); return -1;
const size_t frame_id = (size_t)map.frames[f].frame_id;
const double camera_time = map.frames[f].coordinate_time;
if (report_mesh_incomplete(s, &map.frames[f].mesh, NULL, frame_id,
camera_time, "import", -1, -1)) {
fputs("Imported lens map: failed to allocate ray-failure diagnostics.\n",
stderr);
free(replay_stats);
lens_map_destroy(&map);
return -1;
}
report_unresolved_frame(s, &map.frames[f].mesh, frame_id, camera_time,
&replay_stats[f], 0);
replay_incomplete |=
replay_stats[f].error != 0 ||
replay_stats[f].budget_incomplete_triangles != 0;
}
for (size_t f = 0; f < map.frame_count; ++f) {
if (!boundary_allows_publish(s, &replay_stats[f])) {
free(replay_stats);
lens_map_destroy(&map);
return -1;
}
}
free(replay_stats);
{
FrameTraceStats trace_totals = {0};
for (size_t f = 0; f < map.frame_count; ++f)
+28 -1
View File
@@ -3,9 +3,35 @@
#include <math.h>
#include <stdio.h>
#include <string.h>
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. */
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};
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]);
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);
failed = 1;
}
return failed;
}
static int check_ray(const SpacetimeSource *source,
const ObserverState *observer,
const double local_direction[3],
@@ -36,7 +62,8 @@ int main(void) {
metric.alpha != 1.0 ||
!spacetime_slab_eval(slab, 0.25, (double[]){0.0, 0.0, 0.0}, &metric))
return 1;
int result = check_ray(&source, &observer, (double[]){1.0, 0.0, 0.0},
int result = check_reason_names() ||
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},
(double[]){1.0, 0.0, 0.0});
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""Regression test for always-on ray failure diagnostics.
Every INCOMPLETE ray endpoint must be reported on stderr with its reason and
the affected frame id / camera coordinate time even without ``--verbose``, so a
long movie never needs a rerun to be diagnosed. ``--verbose`` (or any Debug
build) adds a bounded set of representative samples with film/cost
localization. Budget-incomplete ``UNRESOLVED/BUDGET_EXHAUSTED`` frames are
reported separately from numerical ``INCOMPLETE`` failures, and the publication
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
``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
all time slabs.
"""
import os
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
# Keep scratch data inside the pre-approved OpenCode scratch directory.
TMP_ROOT = Path('/tmp/opencode')
TMP_ROOT.mkdir(parents=True, exist_ok=True)
BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve()
TESTDIR = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else BUILD
ENV = dict(os.environ, OMP_NUM_THREADS='4')
# v3 lens-map layout (see src/lens_map.c); only enough to count stored
# INCOMPLETE vertices. The first frame header follows the 40..176 v3
# provenance block.
VERSION_OFFSET = 8
FRAME_COUNT_OFFSET = 32
FRAME_HEADER_START = 176
VERTEX_SIZE = 108
OUTCOME_INDEX = 10
INCOMPLETE_OUTCOME = 3
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.
INJECT = ['--integrator', 'dp54',
'--ode-rtol', '1e-15', '--ode-atol-x', '1e-15',
'--ode-atol-pi', '1e-15', '--ode-atol-l', '1e-15',
'--ode-initial-step', '0.5', '--ode-min-step', '0.5',
'--ode-max-step', '0.5', '--ode-max-rejections', '1']
def run(binary, *args, ok=True, env=ENV):
result = subprocess.run([str(binary), *map(str, args)], env=env,
capture_output=True, text=True)
if (result.returncode == 0) != ok:
raise AssertionError(
f'{binary.name} {args}: rc={result.returncode}\n'
f'stdout:\n{result.stdout}\nstderr:\n{result.stderr}')
return result
def is_debug(result):
return 'Debug build:' in result.stdout
def incomplete_total(stderr):
return sum(int(m) for m in re.findall(r'INCOMPLETE=(\d+)', stderr))
def frame_reported(stderr, frame_id):
return re.search(rf'Ray failures: frame {frame_id} camera_t=', stderr) is not None
def map_incomplete_vertices(path):
data = path.read_bytes()
assert data[:8] == b'GRLENS\x01\x00', f'not a lens map: {path}'
version = struct.unpack_from('<I', data, VERSION_OFFSET)[0]
assert version == 3, f'expected v3 map, got v{version}'
frame_count = struct.unpack_from('<Q', data, FRAME_COUNT_OFFSET)[0]
offset = FRAME_HEADER_START
incomplete = 0
for _ in range(frame_count):
vertices, triangles = struct.unpack_from('<QQ', data, offset + 24)
offset += 48
for _ in range(vertices):
fields = struct.unpack_from('<9dIIIQQQ', data, offset)
offset += VERTEX_SIZE
if fields[OUTCOME_INDEX] == INCOMPLETE_OUTCOME:
incomplete += 1
offset += triangles * 32 + 4 # triangles plus the frame payload CRC
return incomplete
# A required renderer or fixture must not be skipped silently under make test.
binary = BUILD / 'schwarzschild_sky'
assert binary.exists(), f'missing {binary}'
observer_test = TESTDIR / 'test_observer_schwarzschild'
assert observer_test.exists(), f'missing {observer_test}'
help_text = run(binary, '--help').stdout
ext = 'png' if '.png' in help_text else 'ppm'
for option in ('--verbose', '--allow-incomplete', '--observer-track',
'--movie-track-samples', '--lens-map-output', '--lens-map-input',
'--refine-max-level'):
assert option in help_text, (option, help_text)
with tempfile.TemporaryDirectory(prefix='gr-ray-diagnostics-',
dir=str(TMP_ROOT)) as directory:
tmp = Path(directory)
# 1) A normal scene emits no ray-failure or budget diagnostics at all, so a
# clean run is not polluted by the always-on summary. The refinement
# variant exercises the always-installed diagnostics callback across
# finished generations without producing any failure.
normal = tmp / f'normal.{ext}'
normal_run = run(binary, *COMMON, '--output', normal)
assert normal_run.returncode == 0
assert 'Ray failures:' not in normal_run.stderr, normal_run.stderr
assert 'UNRESOLVED/BUDGET_EXHAUSTED' not in normal_run.stderr, normal_run.stderr
refined = tmp / f'refined.{ext}'
refined_run = run(binary, *COMMON, '--refine-max-level', '2',
'--refine-angle-abs-deg', '0.1', '--output', refined)
assert refined_run.returncode == 0
assert 'Ray failures:' not in refined_run.stderr, refined_run.stderr
# 2) Non-verbose INCOMPLETE: the reason histogram, frame id, and camera time
# are on stderr even though --verbose was not passed.
single = tmp / f'single.{ext}'
single_run = run(binary, *COMMON, *INJECT, '--allow-incomplete',
'--output', single)
err = single_run.stderr
assert 'Ray failures:' in err, err
assert 'INCOMPLETE=' in err and 'INTEGRATION_ERROR' in err, err
assert frame_reported(err, 0), err
if not is_debug(single_run):
assert 'ray failure:' not in err, err
assert 'film=' not in err, err
else:
# A Debug build prints representative detail without --verbose.
assert 'ray failure:' in err and 'film=' in err, err
# 3) Verbose adds bounded representative samples with localization.
verbose = tmp / f'verbose.{ext}'
verbose_run = run(binary, *COMMON, *INJECT, '--allow-incomplete', '--verbose',
'--output', verbose)
err = verbose_run.stderr
assert 'ray failure:' in err, err
assert 'reason=INTEGRATION_ERROR' 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
# reported as suppressed.
reps = sum(1 for line in err.splitlines()
if 'ray failure: frame=0 reason=' in line
and 'suppressed=' not in line)
assert reps <= 3, err
assert 'suppressed=' in err, err
# 4) Refinement enabled with the failing trace: the always-installed
# diagnostics callback is part of the build, but the uniform strict
# injection fails every initial vertex, so no probe generation runs and
# only the initial scan reports (documented limit: probe/refinement
# witness failures are not reachable from this deterministic scene).
refine_map = tmp / 'refine_inc.grlens'
refine_run = run(binary, *COMMON, *INJECT, '--refine-max-level', '1',
'--refine-angle-abs-deg', '0.1', '--allow-incomplete',
'--lens-map-output', refine_map,
'--output', tmp / f'refine_inc.{ext}')
err = refine_run.stderr
assert frame_reported(err, 0), err
stored = map_incomplete_vertices(refine_map)
assert stored > 0
assert incomplete_total(err) == stored, \
f'refine histogram {incomplete_total(err)} != stored {stored}'
# 5) Two-frame movie. (a) A default, non-verbose run must identify both
# frames and count each failing sample exactly once across the time
# slabs, matching the saved map.
track_single = tmp / 'track_single.csv'
run(observer_test, track_single)
rows = [line for line in track_single.read_text().splitlines()
if line.strip() and not line.startswith('#')
and not line[0].isalpha()]
assert len(rows) == 1, rows
fields = rows[0].split(',')
fields[0], fields[1] = '1', '1'
header = next(line for line in track_single.read_text().splitlines()
if line.startswith('t,'))
track = tmp / 'track2.csv'
track.write_text(header + '\n' + rows[0] + '\n' + ','.join(fields) + '\n')
frames_dir = tmp / 'frames'
frames_dir.mkdir()
movie_map = tmp / 'movie.grlens'
movie_run = run(binary, *COMMON, *INJECT, '--allow-incomplete',
'--slab-duration', '0.4', '--observer-track', track,
'--movie-track-samples', '--frames-dir', frames_dir,
'--lens-map-output', movie_map)
err = movie_run.stderr
assert frame_reported(err, 0) and frame_reported(err, 1), err
stored = map_incomplete_vertices(movie_map)
assert stored > 0, 'movie map has no INCOMPLETE vertices'
assert incomplete_total(err) == stored, \
f'histogram {incomplete_total(err)} != stored {stored}; ' \
'a sample was re-counted across slabs'
if not is_debug(movie_run):
assert 'ray failure:' not in err, err
# (b) Verbose movie adds request kind, persistent vertex id, film position
# and cost, bounded per reason.
movie_verbose_dir = tmp / 'frames_verbose'
movie_verbose_dir.mkdir()
movie_verbose = run(binary, *COMMON, *INJECT, '--allow-incomplete',
'--verbose', '--slab-duration', '0.4',
'--observer-track', track, '--movie-track-samples',
'--frames-dir', movie_verbose_dir)
err = movie_verbose.stderr
assert 'sample=' in err and 'kind=vertex' in err and 'vertex=' in err, err
assert 'film=(' in err, err
assert 'accepted=' in err and 'rejected=' in err and 'rhs=' in err, err
reps = sum(1 for line in err.splitlines()
if 'ray failure:' in line and 'suppressed=' not in line)
assert reps <= 6, err # two frames, one reason each, <=3 reps per reason
# (c) Render-only replay of the two-frame map without --allow-incomplete
# reports both frames' reasons before the publication gate refuses, and
# never invents an unpersisted trusted stop state.
map_frames_dir = tmp / 'map_frames'
map_frames_dir.mkdir()
refused = run(binary, '--catalog', 'assets/sky_grid_5deg.csv',
'--lens-map-input', movie_map, '--frames-dir', map_frames_dir,
'--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 'phase=import' in err, err
assert 'Incomplete render refused' in err, err
assert err.index('frame 0') < err.index('Incomplete render refused'), err
single_map = tmp / 'single.grlens'
run(binary, *COMMON, *INJECT, '--allow-incomplete', '--lens-map-output',
single_map, '--output', tmp / f'maplive.{ext}')
replay = tmp / f'replay.{ext}'
replay_run = run(binary, '--catalog', 'assets/sky_grid_5deg.csv',
'--lens-map-input', single_map, '--allow-incomplete',
'--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 'stop_t=' not in err and 'trusted=1' not in err, err
# 6) Budget exhaustion is a distinct, always-on message, and its refusal
# still points at the retry budget. A replay of an allowed budget map
# must not present the unpersisted continuation time as observed.
budget_map = tmp / 'budget.grlens'
run(binary, *COMMON, '--integrator', 'dp54',
'--trace-lookback-time', '1e-6', '--retry-lookback-increment', '0',
'--max-total-lookback-time', '1e-6', '--allow-incomplete',
'--lens-map-output', budget_map, '--output', tmp / f'budget_allow.{ext}')
replay_budget = run(binary, '--catalog', 'assets/sky_grid_5deg.csv',
'--lens-map-input', budget_map, '--allow-incomplete',
'--verbose', '--output', tmp / f'budget_replay.{ext}')
err = replay_budget.stderr
assert 'UNRESOLVED/BUDGET_EXHAUSTED' in err, err
assert 'continuation_t=-' in err, err
assert 'continuation_t=-1' not in err, \
'replay invented an unpersisted continuation time'
budget = tmp / f'budget.{ext}'
budget_run = run(binary, *COMMON, '--integrator', 'dp54',
'--trace-lookback-time', '1e-6',
'--retry-lookback-increment', '0',
'--max-total-lookback-time', '1e-6',
'--output', budget, ok=False)
err = budget_run.stderr
assert 'UNRESOLVED/BUDGET_EXHAUSTED' in err, err
assert re.search(r'UNRESOLVED/BUDGET_EXHAUSTED: frame 0 camera_t=', err), err
assert 'blocking_triangles=' in err and 'unresolved_samples=' in err, err
assert 'Incomplete render refused' in err, err
assert 'budget' in err.lower(), err
assert not budget.exists()
print('ray diagnostics checks passed: always-on reasons + frame/time, '
'bounded verbose samples, movie slab de-duplication and replay',
flush=True)
+4
View File
@@ -625,6 +625,10 @@ coverage-based antialiasing.
Normal progress and summaries go to stdout; warnings, errors, and Debug
diagnostics go to stderr. Successful runs exit `0` even if warnings are emitted.
Incomplete ray failures and budget-exhausted frames report their reasons and
affected frames on stderr even without `--verbose`; `--verbose` and Debug builds
add bounded per-sample localization (film position and integration cost).
## HDR output
To preserve a single-frame render for later exposure and tone-mapping work,