Compare commits
3
Commits
b3f1e837d5
...
76ed705d37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ed705d37 | ||
|
|
af8b83007f | ||
|
|
69a8cb5647 |
No files matched your search
@@ -299,6 +299,8 @@ test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ADAPTIVE_GEODESIC_TEST_TARGET) $(A
|
|||||||
$(SENSOR_BLOOM_TEST_TARGET)
|
$(SENSOR_BLOOM_TEST_TARGET)
|
||||||
python3 tests/test_camera_cli.py $(BUILD_DIR) $(TEST_OUT_DIR)
|
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_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)
|
tone-map-test: $(TONE_MAP_TEST_TARGET)
|
||||||
$(TONE_MAP_TEST_TARGET)
|
$(TONE_MAP_TEST_TARGET)
|
||||||
|
|||||||
@@ -381,7 +381,9 @@ typedef struct {
|
|||||||
|
|
||||||
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
|
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
|
||||||
由事件、坐标速度、指向与 roll 生成完整四速度和 tetrad。
|
由事件、坐标速度、指向与 roll 生成完整四速度和 tetrad。
|
||||||
目前解析单张事件取 $t=0$;此构造不要求静态时空或静止观测者。
|
`--observer-time T` 指定单张相机事件的坐标时(默认 $t=0$,接受任意有限值,
|
||||||
|
包括负值);metric 求值、ray 初始化与 lens-map 帧元数据使用同一事件时刻。
|
||||||
|
此构造不要求静态时空或静止观测者;瞬时相机的 proper time 仍以零为参考。
|
||||||
|
|
||||||
`--observer-position X Y Z` 与 `--look-ra-deg` / `--look-dec-deg` 独立。
|
`--observer-position X Y Z` 与 `--look-ra-deg` / `--look-dec-deg` 独立。
|
||||||
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
|
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
|
||||||
@@ -777,6 +779,17 @@ witness 提升为正式 midpoint 时原地复用同一 vertex id,只保留一
|
|||||||
provenance 保存 coordinate-time step 与初始 step 预算,使实际积分来源与成本可
|
provenance 保存 coordinate-time step 与初始 step 预算,使实际积分来源与成本可
|
||||||
replay。
|
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 协议
|
# 18A. 渐近外区、escape worldtube 与 endpoint 协议
|
||||||
|
|||||||
+7
-7
@@ -301,7 +301,7 @@ static void report_group(const DummyPsfSink *sink, int full_only,
|
|||||||
if (!full_only || sink->samples[i].events == sink->event_capacity)
|
if (!full_only || sink->samples[i].events == sink->event_capacity)
|
||||||
++count;
|
++count;
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
fprintf(stderr, "Dummy PSF %s chunks: none\n", label);
|
fprintf(stdout, "Dummy PSF %s chunks: none\n", label);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double *events = count > SIZE_MAX / (6 * sizeof *events)
|
double *events = count > SIZE_MAX / (6 * sizeof *events)
|
||||||
@@ -338,7 +338,7 @@ static void report_group(const DummyPsfSink *sink, int full_only,
|
|||||||
"maximum_successive_triangle_jump_px"};
|
"maximum_successive_triangle_jump_px"};
|
||||||
for (size_t field = 0; field < 6; ++field) {
|
for (size_t field = 0; field < 6; ++field) {
|
||||||
qsort(series[field], count, sizeof **series, compare_double);
|
qsort(series[field], count, sizeof **series, compare_double);
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
||||||
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
||||||
label, names[field], series[field][0],
|
label, names[field], series[field][0],
|
||||||
@@ -349,7 +349,7 @@ static void report_group(const DummyPsfSink *sink, int full_only,
|
|||||||
percentile(series[field], count, 0.90),
|
percentile(series[field], count, 0.90),
|
||||||
percentile(series[field], count, 0.99), series[field][count - 1]);
|
percentile(series[field], count, 0.99), series[field][count - 1]);
|
||||||
}
|
}
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF %s adaptive criterion: %zu/%zu chunks tile16-eligible "
|
"Dummy PSF %s adaptive criterion: %zu/%zu chunks tile16-eligible "
|
||||||
"(events>=8192 and events/occupied_32px_tiles>=32)\n",
|
"(events>=8192 and events/occupied_32px_tiles>=32)\n",
|
||||||
label, adaptive, count);
|
label, adaptive, count);
|
||||||
@@ -386,7 +386,7 @@ static void report_support_group(const DummyPsfSink *sink, int full_only,
|
|||||||
for (size_t field = 0; field < 5; ++field) {
|
for (size_t field = 0; field < 5; ++field) {
|
||||||
double *series = values + field * count;
|
double *series = values + field * count;
|
||||||
qsort(series, count, sizeof *series, compare_double);
|
qsort(series, count, sizeof *series, compare_double);
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
||||||
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
||||||
label, names[field], series[0],
|
label, names[field], series[0],
|
||||||
@@ -395,7 +395,7 @@ static void report_support_group(const DummyPsfSink *sink, int full_only,
|
|||||||
percentile(series, count, 0.90), percentile(series, count, 0.99),
|
percentile(series, count, 0.90), percentile(series, count, 0.99),
|
||||||
series[count - 1]);
|
series[count - 1]);
|
||||||
}
|
}
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF %s support totals: references=%zu tasks=%zu summed_first_pass=%.3f s\n",
|
"Dummy PSF %s support totals: references=%zu tasks=%zu summed_first_pass=%.3f s\n",
|
||||||
label, total_refs, total_tasks, total_pass);
|
label, total_refs, total_tasks, total_pass);
|
||||||
free(values);
|
free(values);
|
||||||
@@ -407,7 +407,7 @@ void dummy_psf_sink_report(const DummyPsfSink *sink) {
|
|||||||
size_t full = 0;
|
size_t full = 0;
|
||||||
for (size_t i = 0; i < sink->count; ++i)
|
for (size_t i = 0; i < sink->count; ++i)
|
||||||
full += sink->samples[i].events == sink->event_capacity;
|
full += sink->samples[i].events == sink->event_capacity;
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF diagnostic only: no HDR or image was accumulated/written.\n"
|
"Dummy PSF diagnostic only: no HDR or image was accumulated/written.\n"
|
||||||
"Dummy PSF chunks: total=%zu full=%zu partial=%zu events=%zu "
|
"Dummy PSF chunks: total=%zu full=%zu partial=%zu events=%zu "
|
||||||
"capacity=%zu selector_tile=%dpx\n",
|
"capacity=%zu selector_tile=%dpx\n",
|
||||||
@@ -416,7 +416,7 @@ void dummy_psf_sink_report(const DummyPsfSink *sink) {
|
|||||||
report_group(sink, 0, "all");
|
report_group(sink, 0, "all");
|
||||||
report_group(sink, 1, "full");
|
report_group(sink, 1, "full");
|
||||||
if (sink->support_stats) {
|
if (sink->support_stats) {
|
||||||
fputs("Dummy PSF support diagnostic: 16px rectangular cache-limited first pass only; no references were materialized or uploaded.\n", stderr);
|
fputs("Dummy PSF support diagnostic: 16px rectangular cache-limited first pass only; no references were materialized or uploaded.\n", stdout);
|
||||||
report_support_group(sink, 0, "all");
|
report_support_group(sink, 0, "all");
|
||||||
report_support_group(sink, 1, "full");
|
report_support_group(sink, 1, "full");
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2260,7 +2260,7 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh,
|
|||||||
(size_t)dummy_workers, completed, 1);
|
(size_t)dummy_workers, completed, 1);
|
||||||
}
|
}
|
||||||
if (psf_event_sink_destroy(&owner)) dummy_failed = 1;
|
if (psf_event_sink_destroy(&owner)) dummy_failed = 1;
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Dummy PSF producers: %d workers; classification/chunk wall %.3f s\n",
|
"Dummy PSF producers: %d workers; classification/chunk wall %.3f s\n",
|
||||||
dummy_workers, omp_get_wtime() - dummy_start);
|
dummy_workers, omp_get_wtime() - dummy_start);
|
||||||
copy_psf_splat_stats(psf_stats, (CatalogSplatStats){
|
copy_psf_splat_stats(psf_stats, (CatalogSplatStats){
|
||||||
@@ -2349,14 +2349,14 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh,
|
|||||||
}
|
}
|
||||||
omp_destroy_lock(&submit_lock);
|
omp_destroy_lock(&submit_lock);
|
||||||
if (psf_event_sink_destroy(&owner)) hip_failed = 1;
|
if (psf_event_sink_destroy(&owner)) hip_failed = 1;
|
||||||
fprintf(stderr, "HIP producers: %d workers; summed generation %.3f s, submission/fallback %.3f s; wall %.3f s\n",
|
fprintf(stdout, "HIP producers: %d workers; summed generation %.3f s, submission/fallback %.3f s; wall %.3f s\n",
|
||||||
hip_workers, hip_generate_seconds, hip_submit_seconds, omp_get_wtime() - hip_start);
|
hip_workers, hip_generate_seconds, hip_submit_seconds, omp_get_wtime() - hip_start);
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"HIP PSF accumulation: atomic %zu batches, tile16 %zu batches; selector %.3f s, binning %.3f s\n",
|
"HIP PSF accumulation: atomic %zu batches, tile16 %zu batches; selector %.3f s, binning %.3f s\n",
|
||||||
owner.hip_timing.atomic_batch_count, owner.hip_timing.tile_batch_count,
|
owner.hip_timing.atomic_batch_count, owner.hip_timing.tile_batch_count,
|
||||||
owner.hip_timing.selection_seconds, owner.hip_timing.bin_seconds);
|
owner.hip_timing.selection_seconds, owner.hip_timing.bin_seconds);
|
||||||
if (owner.hip_timing.tile_batch_count)
|
if (owner.hip_timing.tile_batch_count)
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"HIP PSF tile workload: references %zu, tasks %zu, merges %zu; peak references/chunk %zu\n",
|
"HIP PSF tile workload: references %zu, tasks %zu, merges %zu; peak references/chunk %zu\n",
|
||||||
owner.hip_timing.tile_reference_count, owner.hip_timing.tile_task_count,
|
owner.hip_timing.tile_reference_count, owner.hip_timing.tile_task_count,
|
||||||
owner.hip_timing.tile_merge_count,
|
owner.hip_timing.tile_merge_count,
|
||||||
|
|||||||
@@ -9,6 +9,32 @@ typedef struct {
|
|||||||
double x[3], Pi[3], log_alpha_p0;
|
double x[3], Pi[3], log_alpha_p0;
|
||||||
} Derivative;
|
} 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]) {
|
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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ typedef enum {
|
|||||||
RAY_REASON_IO_ERROR
|
RAY_REASON_IO_ERROR
|
||||||
} RayReason;
|
} 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`
|
/* 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
@@ -605,7 +605,7 @@ static int submit_prepared(HipPsfSink *sink, HipPsfPreparedChunk *prepared,
|
|||||||
sink->timing.event_count += event_count;
|
sink->timing.event_count += event_count;
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (std::chrono::duration<double>(now - sink->last_report).count() >= 5.0) {
|
if (std::chrono::duration<double>(now - sink->last_report).count() >= 5.0) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stdout,
|
||||||
"HIP PSF progress: %zu submitted / %zu completed events; %zu / %zu batches timed; kernel %.3f s\n",
|
"HIP PSF progress: %zu submitted / %zu completed events; %zu / %zu batches timed; kernel %.3f s\n",
|
||||||
sink->timing.event_count, sink->completed_events,
|
sink->timing.event_count, sink->completed_events,
|
||||||
sink->timing.timed_batch_count, sink->timing.batch_count,
|
sink->timing.timed_batch_count, sink->timing.batch_count,
|
||||||
@@ -641,7 +641,7 @@ static int submit_prepared(HipPsfSink *sink, HipPsfPreparedChunk *prepared,
|
|||||||
sink->timing.event_count += event_count;
|
sink->timing.event_count += event_count;
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (std::chrono::duration<double>(now - sink->last_report).count() >= 5.0) {
|
if (std::chrono::duration<double>(now - sink->last_report).count() >= 5.0) {
|
||||||
std::fprintf(stderr, "HIP PSF progress: %zu submitted / %zu completed events; %zu / %zu batches timed; kernel %.3f s\n",
|
std::fprintf(stdout, "HIP PSF progress: %zu submitted / %zu completed events; %zu / %zu batches timed; kernel %.3f s\n",
|
||||||
sink->timing.event_count, sink->completed_events, sink->timing.timed_batch_count,
|
sink->timing.event_count, sink->completed_events, sink->timing.timed_batch_count,
|
||||||
sink->timing.batch_count, sink->timing.kernel_seconds);
|
sink->timing.batch_count, sink->timing.kernel_seconds);
|
||||||
sink->last_report = now;
|
sink->last_report = now;
|
||||||
|
|||||||
+715
-99
File diff suppressed because it is too large.
Load diff
+4
-4
@@ -16,23 +16,23 @@ static int movie_output_default_write(void *context, const MovieOutputJob *job,
|
|||||||
if (write_rgb8_image(job->output_path, job->clean_rgb8, job->width,
|
if (write_rgb8_image(job->output_path, job->clean_rgb8, job->width,
|
||||||
job->height, settings))
|
job->height, settings))
|
||||||
return -1;
|
return -1;
|
||||||
fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (ok%s)\n",
|
fprintf(stdout, "Rendered %zu images from %zu catalog stars to %s (ok%s)\n",
|
||||||
job->images, job->catalog_stars, job->output_path, job->note);
|
job->images, job->catalog_stars, job->output_path, job->note);
|
||||||
if (job->draw_mesh && job->mesh_rgb8 != NULL) {
|
if (job->draw_mesh && job->mesh_rgb8 != NULL) {
|
||||||
if (write_rgb8_image(job->mesh_path, job->mesh_rgb8, job->width,
|
if (write_rgb8_image(job->mesh_path, job->mesh_rgb8, job->width,
|
||||||
job->height, settings))
|
job->height, settings))
|
||||||
return -1;
|
return -1;
|
||||||
fprintf(stderr, "Wrote mesh overlay image: %s\n", job->mesh_path);
|
fprintf(stdout, "Wrote mesh overlay image: %s\n", job->mesh_path);
|
||||||
}
|
}
|
||||||
if (job->has_psf_stats) {
|
if (job->has_psf_stats) {
|
||||||
if (job->fast_mode)
|
if (job->fast_mode)
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Fast PSF splats: deposited %zu, wing-clipped %zu, discarded "
|
"Fast PSF splats: deposited %zu, wing-clipped %zu, discarded "
|
||||||
"below min-Y %zu\n",
|
"below min-Y %zu\n",
|
||||||
job->psf_stats.cached_splats, job->psf_stats.cached_wing_clipped,
|
job->psf_stats.cached_splats, job->psf_stats.cached_wing_clipped,
|
||||||
job->psf_stats.discarded_below_min_y);
|
job->psf_stats.discarded_below_min_y);
|
||||||
else
|
else
|
||||||
psf_kernel_cache_report(NULL, &job->psf_stats, stderr);
|
psf_kernel_cache_report(NULL, &job->psf_stats, stdout);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -251,7 +251,7 @@ void psf_kernel_cache_report(const PsfKernelCache *cache,
|
|||||||
stats->gpu_upload_seconds, stats->gpu_kernel_seconds,
|
stats->gpu_upload_seconds, stats->gpu_kernel_seconds,
|
||||||
stats->gpu_download_seconds);
|
stats->gpu_download_seconds);
|
||||||
if (stats != NULL && stats->discarded_below_min_y != 0)
|
if (stats != NULL && stats->discarded_below_min_y != 0)
|
||||||
fputs("Warning: --psf-min-y discarded one or more PSF events.\n", stream);
|
fputs("Warning: --psf-min-y discarded one or more PSF events.\n", stderr);
|
||||||
#ifdef GR_DEBUG
|
#ifdef GR_DEBUG
|
||||||
if (stats != NULL)
|
if (stats != NULL)
|
||||||
fprintf(stream, "Debug: max raw magnification %.6g; magnification-clamped "
|
fprintf(stream, "Debug: max raw magnification %.6g; magnification-clamped "
|
||||||
@@ -705,7 +705,7 @@ int fast_psf_accumulator_resolve(FastPsfAccumulator *accumulator,
|
|||||||
accumulator->fftw_last_timing = timing;
|
accumulator->fftw_last_timing = timing;
|
||||||
accumulator->fftw_frame_seconds = omp_get_wtime() - start;
|
accumulator->fftw_frame_seconds = omp_get_wtime() - start;
|
||||||
if (accumulator->verbose)
|
if (accumulator->verbose)
|
||||||
fprintf(stderr,
|
fprintf(stdout,
|
||||||
"Fast FFTW frame: zero_pack=%.6f forward=%.6f "
|
"Fast FFTW frame: zero_pack=%.6f forward=%.6f "
|
||||||
"multiply=%.6f inverse=%.6f crop_downsample=%.6f "
|
"multiply=%.6f inverse=%.6f crop_downsample=%.6f "
|
||||||
"total=%.6f\n",
|
"total=%.6f\n",
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ def endpoint_deviation(a, b):
|
|||||||
return mismatches, worst
|
return mismatches, worst
|
||||||
|
|
||||||
|
|
||||||
def trace_cost(stderr, label):
|
def trace_cost(text, label):
|
||||||
match = re.search(label + r' trace cost: accepted=(\d+) rejected=(\d+) '
|
match = re.search(label + r' trace cost: accepted=(\d+) rejected=(\d+) '
|
||||||
r'rhs=(\d+)', stderr)
|
r'rhs=(\d+)', text)
|
||||||
return None if match is None else match.groups()
|
return None if match is None else match.groups()
|
||||||
|
|
||||||
|
|
||||||
@@ -263,8 +263,8 @@ with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
|
|||||||
replay_hdr = replay_out.with_name(replay_out.stem + '_HDR.fits')
|
replay_hdr = replay_out.with_name(replay_out.stem + '_HDR.fits')
|
||||||
assert dflt_out.with_name(dflt_out.stem + '_HDR.fits').read_bytes() \
|
assert dflt_out.with_name(dflt_out.stem + '_HDR.fits').read_bytes() \
|
||||||
== replay_hdr.read_bytes()
|
== replay_hdr.read_bytes()
|
||||||
live_cost = trace_cost(dflt_run.stderr, 'Frame 0')
|
live_cost = trace_cost(dflt_run.stdout, 'Frame 0')
|
||||||
replay_cost = trace_cost(replay_run.stderr, 'Imported map')
|
replay_cost = trace_cost(replay_run.stdout, 'Imported map')
|
||||||
assert live_cost is not None and replay_cost is not None
|
assert live_cost is not None and replay_cost is not None
|
||||||
assert live_cost == replay_cost, (live_cost, replay_cost)
|
assert live_cost == replay_cost, (live_cost, replay_cost)
|
||||||
|
|
||||||
|
|||||||
+57
-11
@@ -115,14 +115,15 @@ def map_vertices(path):
|
|||||||
return values, data[offset:offset + triangles * MAP_TRIANGLE_SIZE]
|
return values, data[offset:offset + triangles * MAP_TRIANGLE_SIZE]
|
||||||
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-', dir='/tmp/opencode') as directory:
|
||||||
tmp = Path(directory)
|
tmp = Path(directory)
|
||||||
for backend in ('minkowski', 'schwarzschild'):
|
for backend in ('minkowski', 'schwarzschild'):
|
||||||
binary = BUILD / f'{backend}_sky'
|
binary = BUILD / f'{backend}_sky'
|
||||||
help_text = run(binary, '--help').stdout
|
help_text = run(binary, '--help').stdout
|
||||||
ext = 'png' if '.png' in help_text else 'ppm'
|
ext = 'png' if '.png' in help_text else 'ppm'
|
||||||
hdr_available = '--hdr-output' in help_text
|
hdr_available = any(line.startswith(' --hdr-output ')
|
||||||
for option in ('--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
for line in help_text.splitlines())
|
||||||
|
for option in ('--observer-time', '--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
||||||
assert option in help_text
|
assert option in help_text
|
||||||
assert '--tone-map' in help_text and '--tone-map-p' in help_text
|
assert '--tone-map' in help_text and '--tone-map-p' in help_text
|
||||||
assert '--sensor-bloom-limit' in help_text
|
assert '--sensor-bloom-limit' in help_text
|
||||||
@@ -149,7 +150,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
fast_path = tmp / f'minkowski_fast.{ext}'
|
fast_path = tmp / f'minkowski_fast.{ext}'
|
||||||
fast = run(binary, *common, '--fast-mode', '--fast-supersample', 2,
|
fast = run(binary, *common, '--fast-mode', '--fast-supersample', 2,
|
||||||
'--output', fast_path)
|
'--output', fast_path)
|
||||||
assert 'Fast FFTW:' in fast.stderr, fast.stderr
|
assert 'Fast FFTW:' in fast.stdout, fast.stdout
|
||||||
assert image_payload(fast_path)
|
assert image_payload(fast_path)
|
||||||
|
|
||||||
# Equivalent independently specified and inferred camera geometry.
|
# Equivalent independently specified and inferred camera geometry.
|
||||||
@@ -162,6 +163,15 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
assert pole == render('pole_explicit', '--observer-position', 0, 0, 30,
|
assert pole == render('pole_explicit', '--observer-position', 0, 0, 30,
|
||||||
'--look-ra-deg', 0, '--look-dec-deg', -90)
|
'--look-ra-deg', 0, '--look-dec-deg', -90)
|
||||||
default = render('default')
|
default = render('default')
|
||||||
|
# Stationary backends are time-translation invariant; the event time
|
||||||
|
# must nevertheless survive in lens-map metadata, including negatives.
|
||||||
|
for time in (0, 12.5, -12.5):
|
||||||
|
timed_map = tmp / f'{backend}_time_{time}.grlens'
|
||||||
|
assert default == render('timed', '--observer-time', time,
|
||||||
|
'--lens-map-output', timed_map)
|
||||||
|
saved_time, proper_time = struct.unpack_from(
|
||||||
|
'<dd', timed_map.read_bytes(), MAP_FRAME_HEADER_START + 8)
|
||||||
|
assert saved_time == time and proper_time == 0
|
||||||
pos = (0, 0, 0) if backend == 'minkowski' else (0, 0, 30)
|
pos = (0, 0, 0) if backend == 'minkowski' else (0, 0, 30)
|
||||||
assert default == render('default_explicit', '--observer-position', *pos,
|
assert default == render('default_explicit', '--observer-position', *pos,
|
||||||
'--look-ra-deg', 90, '--look-dec-deg', -90)
|
'--look-ra-deg', 90, '--look-dec-deg', -90)
|
||||||
@@ -217,10 +227,19 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
tmp / 'missing_catalog.csv', '--output', long_path,
|
tmp / 'missing_catalog.csv', '--output', long_path,
|
||||||
ok=False)
|
ok=False)
|
||||||
assert 'Mesh overlay output path is too long' in too_long.stderr, too_long.stderr
|
assert 'Mesh overlay output path is too long' in too_long.stderr, too_long.stderr
|
||||||
assert 'Blackbody backend' not in too_long.stderr, too_long.stderr
|
assert 'Blackbody backend' not in (too_long.stdout + too_long.stderr), too_long.stderr
|
||||||
assert 'PSF cache ready' not in too_long.stderr
|
assert 'PSF cache ready' not in (too_long.stdout + too_long.stderr)
|
||||||
|
|
||||||
errors = [
|
errors = [
|
||||||
|
(['--observer-time'], None),
|
||||||
|
(['--observer-time', ''], None),
|
||||||
|
(['--observer-time', 'bad'], None),
|
||||||
|
(['--observer-time', 'nan'], None),
|
||||||
|
(['--observer-time', 'inf'], None),
|
||||||
|
(['--observer-time', '-inf'], None),
|
||||||
|
(['--observer-track', 'missing.csv', '--observer-time', 0], 'cannot be combined'),
|
||||||
|
(['--frames-dir', tmp, '--observer-time', 0], 'cannot be combined'),
|
||||||
|
(['--lens-map-input', 'missing.grlens', '--observer-time', 0], 'cannot be combined'),
|
||||||
(['--observer-position', 1, 2], None),
|
(['--observer-position', 1, 2], None),
|
||||||
(['--observer-position', 1, 2, 'nan'], None),
|
(['--observer-position', 1, 2, 'nan'], None),
|
||||||
(['--observer-velocity', 0, 0, 'inf'], None),
|
(['--observer-velocity', 0, 0, 'inf'], None),
|
||||||
@@ -275,7 +294,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
if message:
|
if message:
|
||||||
assert message in result.stderr, result.stderr
|
assert message in result.stderr, result.stderr
|
||||||
assert not missing_catalog.exists(), result.stderr
|
assert not missing_catalog.exists(), result.stderr
|
||||||
assert 'PSF cache ready' not in result.stderr
|
assert 'PSF cache ready' not in (result.stdout + result.stderr)
|
||||||
|
|
||||||
track = tmp / f'{backend}.csv'
|
track = tmp / f'{backend}.csv'
|
||||||
run(TESTDIR / f'test_observer_{backend}', track)
|
run(TESTDIR / f'test_observer_{backend}', track)
|
||||||
@@ -378,8 +397,8 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
'--sensor-bloom-transfer', 0.5,
|
'--sensor-bloom-transfer', 0.5,
|
||||||
'--output', bloom_output)
|
'--output', bloom_output)
|
||||||
assert image_payload(bloom_output) != baseline
|
assert image_payload(bloom_output) != baseline
|
||||||
assert 'Sensor bloom:' in bloom_run.stderr, bloom_run.stderr
|
assert 'Sensor bloom:' in bloom_run.stdout, bloom_run.stdout
|
||||||
report = bloom_run.stderr.split('Sensor bloom:', 1)[1].splitlines()[0]
|
report = bloom_run.stdout.split('Sensor bloom:', 1)[1].splitlines()[0]
|
||||||
fields = dict(token.split('=', 1) for token in report.split() if '=' in token)
|
fields = dict(token.split('=', 1) for token in report.split() if '=' in token)
|
||||||
assert int(fields['saturated']) > 0, report
|
assert int(fields['saturated']) > 0, report
|
||||||
assert int(fields['iterations'].split('/')[0]) >= 1, report
|
assert int(fields['iterations'].split('/')[0]) >= 1, report
|
||||||
@@ -424,8 +443,8 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
'--movie-track-samples', '--frames-dir', tmp,
|
'--movie-track-samples', '--frames-dir', tmp,
|
||||||
'--frames-prefix', 'mixed', '--verbose',
|
'--frames-prefix', 'mixed', '--verbose',
|
||||||
'--lens-map-output', parallel_map)
|
'--lens-map-output', parallel_map)
|
||||||
assert 'Ray trace generation 1: frame 0 added' in result.stderr
|
assert 'Ray trace generation 1: frame 0 added' in result.stdout
|
||||||
assert 'Ray trace generation 0: frame 1 added' not in result.stderr
|
assert 'Ray trace generation 0: frame 1 added' not in result.stdout
|
||||||
for frame in range(2):
|
for frame in range(2):
|
||||||
image_payload(tmp / f'mixed_{frame:06d}.{ext}',
|
image_payload(tmp / f'mixed_{frame:06d}.{ext}',
|
||||||
dimensions=(64, 36), allow_black=True)
|
dimensions=(64, 36), allow_black=True)
|
||||||
@@ -443,3 +462,30 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
|||||||
f'movie lens map changed with {threads} threads'
|
f'movie lens map changed with {threads} threads'
|
||||||
print('schwarzschild: movie lens map identical with 1, 4 and 16 threads', flush=True)
|
print('schwarzschild: movie lens map identical with 1, 4 and 16 threads', flush=True)
|
||||||
print(f'{backend}: CLI checks passed; single/movie image identical, map max error {max_error:.3g}', flush=True)
|
print(f'{backend}: CLI checks passed; single/movie image identical, map max error {max_error:.3g}', flush=True)
|
||||||
|
|
||||||
|
# In the moving bubble, (t, x) -> (t+T, x+v_s*T) preserves the
|
||||||
|
# metric and physical ray endpoints. This detects a stale t=0 in either
|
||||||
|
# camera metric evaluation or ray initialization, not just map metadata.
|
||||||
|
alc = BUILD / 'alcubierre_sky'
|
||||||
|
if alc.exists():
|
||||||
|
reference = None
|
||||||
|
for time in (0, 12.5, -12.5):
|
||||||
|
path = tmp / f'alcubierre_time_{time}.grlens'
|
||||||
|
run(alc, *common, '--alcubierre-vs', 0.3,
|
||||||
|
'--alcubierre-radius', 1, '--observer-time', time,
|
||||||
|
'--observer-position', 0.3 * time, 0, 0,
|
||||||
|
'--observer-velocity', 0.3, 0, 0,
|
||||||
|
'--look-ra-deg', 0, '--look-dec-deg', 0,
|
||||||
|
'--lens-map-output', path, '--output', tmp / 'alcubierre.png')
|
||||||
|
assert struct.unpack_from('<d', path.read_bytes(),
|
||||||
|
MAP_FRAME_HEADER_START + 8)[0] == time
|
||||||
|
vertices, triangles = map_vertices(path)
|
||||||
|
if reference is None:
|
||||||
|
reference = vertices, triangles
|
||||||
|
continue
|
||||||
|
expected, expected_triangles = reference
|
||||||
|
assert len(vertices) == len(expected) and triangles == expected_triangles
|
||||||
|
for actual, baseline in zip(vertices, expected):
|
||||||
|
assert actual[9:12] == baseline[9:12]
|
||||||
|
assert max(abs(a - b) for a, b in zip(actual[:9], baseline[:9])) < 1e-8
|
||||||
|
print('alcubierre: nonzero camera-time translation checks passed', flush=True)
|
||||||
+28
-1
@@ -3,9 +3,35 @@
|
|||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
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
|
||||||
|
* 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,
|
static int check_ray(const SpacetimeSource *source,
|
||||||
const ObserverState *observer,
|
const ObserverState *observer,
|
||||||
const double local_direction[3],
|
const double local_direction[3],
|
||||||
@@ -36,7 +62,8 @@ int main(void) {
|
|||||||
metric.alpha != 1.0 ||
|
metric.alpha != 1.0 ||
|
||||||
!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_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}) ||
|
(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},
|
||||||
(double[]){1.0, 0.0, 0.0});
|
(double[]){1.0, 0.0, 0.0});
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression test for the stdout/stderr split of the production CLI.
|
||||||
|
|
||||||
|
Normal progress, startup/backend configuration, successful file outputs,
|
||||||
|
timings and cache summaries are normal informational output and must go to
|
||||||
|
stdout. Genuine warnings, errors and the Debug per-event direct-fallback
|
||||||
|
diagnostic go to stderr. A mixed success/error line such as
|
||||||
|
``Rendered ... (ok|write failed)`` must follow its outcome.
|
||||||
|
|
||||||
|
The checks capture the two streams separately so that a future change which
|
||||||
|
silently moves a normal message to stderr (or an error to stdout) is caught.
|
||||||
|
Small deterministic Minkowski scenes keep the runtime short.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
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()
|
||||||
|
ENV = dict(os.environ, OMP_NUM_THREADS='4')
|
||||||
|
|
||||||
|
COMMON = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', 16, '--height', 8,
|
||||||
|
'--fov-deg', 80, '--exposure', 1e-3, '--coarse-cell-pixels', 8,
|
||||||
|
'--refine-max-level', 0, '--psf-relative-tail', 1e-4]
|
||||||
|
|
||||||
|
|
||||||
|
def run(binary, *args, env=ENV):
|
||||||
|
return subprocess.run([str(binary), *map(str, args)], env=env,
|
||||||
|
capture_output=True, text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def stderr_errors(result):
|
||||||
|
"""Non-advisory stderr lines.
|
||||||
|
|
||||||
|
A Debug build deliberately logs each direct PSF fallback from the active
|
||||||
|
splat worker; those ``Debug:`` lines are real diagnostics, not a misplaced
|
||||||
|
normal message, so they are excluded when the Debug startup banner is
|
||||||
|
present on stdout.
|
||||||
|
"""
|
||||||
|
lines = [line for line in result.stderr.splitlines() if line.strip()]
|
||||||
|
if 'Debug build:' in result.stdout:
|
||||||
|
lines = [line for line in lines if not line.startswith('Debug:')]
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
binary = BUILD / 'minkowski_sky'
|
||||||
|
assert binary.exists(), f'missing {binary}'
|
||||||
|
|
||||||
|
# The image extension follows the build's compiled writer, exactly as
|
||||||
|
# tests/test_camera_cli.py detects it from --help (a libpng build advertises
|
||||||
|
# `.png`, a PNG-less build advertises `.ppm`).
|
||||||
|
help_text = run(binary, '--help').stdout
|
||||||
|
ext = 'png' if '.png' in help_text else 'ppm'
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix='gr-output-streams-',
|
||||||
|
dir=str(TMP_ROOT)) as directory:
|
||||||
|
tmp = Path(directory)
|
||||||
|
|
||||||
|
# 1) --help is informational: complete usage on stdout, nothing on stderr.
|
||||||
|
help_result = run(binary, '--help')
|
||||||
|
assert help_result.returncode == 0, help_result.stderr
|
||||||
|
assert 'Usage:' in help_result.stdout, help_result.stdout
|
||||||
|
assert help_result.stderr == '', help_result.stderr
|
||||||
|
|
||||||
|
# 2) An unknown option is an error: usage diagnostic on stderr only.
|
||||||
|
bad_result = run(binary, '--not-an-option')
|
||||||
|
assert bad_result.returncode != 0, bad_result.stdout
|
||||||
|
assert bad_result.stdout == '', bad_result.stdout
|
||||||
|
assert bad_result.stderr != '', 'missing error diagnostic on stderr'
|
||||||
|
|
||||||
|
# 3) A successful non-verbose single-frame render puts every startup,
|
||||||
|
# statistics and success line on stdout with an empty stderr.
|
||||||
|
single_out = tmp / f'single.{ext}'
|
||||||
|
single = run(binary, *COMMON, '--output', single_out)
|
||||||
|
assert single.returncode == 0, single.stderr
|
||||||
|
assert single_out.exists(), single.stderr
|
||||||
|
assert 'Blackbody backend:' in single.stdout, single.stdout
|
||||||
|
assert 'PSF cache ready:' in single.stdout, single.stdout
|
||||||
|
assert 'Rendered' in single.stdout and '(ok)' in single.stdout, single.stdout
|
||||||
|
assert '(write failed)' not in single.stdout, single.stdout
|
||||||
|
assert not stderr_errors(single), single.stderr
|
||||||
|
|
||||||
|
# 4) Verbose progress (including the trace-cost line) is still stdout.
|
||||||
|
verbose_out = tmp / f'verbose.{ext}'
|
||||||
|
verbose = run(binary, *COMMON, '--verbose', '--output', verbose_out)
|
||||||
|
assert verbose.returncode == 0, verbose.stderr
|
||||||
|
assert 'Frame 0: tracing' in verbose.stdout, verbose.stdout
|
||||||
|
assert 'Frame 0 trace cost:' in verbose.stdout, verbose.stdout
|
||||||
|
assert not stderr_errors(verbose), verbose.stderr
|
||||||
|
|
||||||
|
# 5) A short movie exercises the asynchronous writer; its per-frame logs,
|
||||||
|
# timing summary and writer summary are stdout, stderr stays empty. The
|
||||||
|
# 2 s fixture track plus --duration 1 --fps 1 yields two frames, so the
|
||||||
|
# async producer/writer overlap is actually exercised.
|
||||||
|
track = tmp / 'track.csv'
|
||||||
|
track_run = run(binary, '--write-minkowski-accel-track', track,
|
||||||
|
'--duration', 2, '--fps', 30, '--proper-acceleration', 1.52)
|
||||||
|
assert track_run.returncode == 0, track_run.stderr
|
||||||
|
assert track.exists(), track_run.stderr
|
||||||
|
frames_dir = tmp / 'frames'
|
||||||
|
frames_dir.mkdir()
|
||||||
|
movie = run(binary, *COMMON, '--observer-track', track, '--frames-dir',
|
||||||
|
frames_dir, '--frames-prefix', 'frame', '--duration', 1,
|
||||||
|
'--fps', 1, '--verbose', '--output', tmp / f'movie.{ext}')
|
||||||
|
assert movie.returncode == 0, movie.stderr
|
||||||
|
assert (frames_dir / f'frame_000000.{ext}').exists(), movie.stderr
|
||||||
|
assert (frames_dir / f'frame_000001.{ext}').exists(), movie.stderr
|
||||||
|
assert 'Ray trace generation' in movie.stdout, movie.stdout
|
||||||
|
assert movie.stdout.count('Rendered') == 2, movie.stdout
|
||||||
|
assert movie.stdout.count('(ok)') == 2, movie.stdout
|
||||||
|
assert 'Movie timing total' in movie.stdout, movie.stdout
|
||||||
|
assert 'Movie writer summary:' in movie.stdout, movie.stdout
|
||||||
|
assert 'Movie end-to-end wall:' in movie.stdout, movie.stdout
|
||||||
|
assert not stderr_errors(movie), movie.stderr
|
||||||
|
|
||||||
|
# 6) A genuine warning goes to stderr and does not disturb the success line
|
||||||
|
# on stdout. The fast-mode preview advisory is deterministic in the CPU
|
||||||
|
# build.
|
||||||
|
fast_out = tmp / f'fast.{ext}'
|
||||||
|
fast = run(binary, *COMMON, '--fast-mode', '--output', fast_out)
|
||||||
|
assert fast.returncode == 0, fast.stderr
|
||||||
|
assert 'Fast mode is a preview approximation' in fast.stderr, fast.stderr
|
||||||
|
assert fast.stdout.count('Rendered') == 1, fast.stdout
|
||||||
|
assert '(ok)' in fast.stdout, fast.stdout
|
||||||
|
# The only stderr content is the advisory: no normal line leaked across.
|
||||||
|
assert len(stderr_errors(fast)) == 1, fast.stderr
|
||||||
|
|
||||||
|
# 7) A failed write must route the mixed success/error line to stderr and
|
||||||
|
# leave stdout free of the success wording.
|
||||||
|
missing_dir = tmp / 'missing_subdir' / f'out.{ext}'
|
||||||
|
failed = run(binary, *COMMON, '--output', missing_dir)
|
||||||
|
assert failed.returncode != 0, failed.stdout
|
||||||
|
assert '(write failed)' in failed.stderr, failed.stderr
|
||||||
|
assert '(write failed)' not in failed.stdout, failed.stdout
|
||||||
|
|
||||||
|
# 8) A rejected camera velocity is an error on stderr, not stdout.
|
||||||
|
velocity = run(binary, *COMMON, '--observer-velocity', 10, 0, 0,
|
||||||
|
'--output', tmp / f'velocity.{ext}')
|
||||||
|
assert velocity.returncode != 0, velocity.stdout
|
||||||
|
assert 'not timelike' in velocity.stderr, velocity.stderr
|
||||||
|
# stdout may hold only the Debug startup banner; no render ran.
|
||||||
|
assert 'Rendered' not in velocity.stdout, velocity.stdout
|
||||||
|
|
||||||
|
print('output-stream checks passed: normal success stdout / diagnostics '
|
||||||
|
'stderr', flush=True)
|
||||||
@@ -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)
|
||||||
@@ -18,10 +18,14 @@ for the field and camera.
|
|||||||
|
|
||||||
Both backends accept the same instantaneous camera parameters. Position and
|
Both backends accept the same instantaneous camera parameters. Position and
|
||||||
velocity use the backend's coordinates; velocity means `dx/dt, dy/dt, dz/dt`,
|
velocity use the backend's coordinates; velocity means `dx/dt, dy/dt, dz/dt`,
|
||||||
not a local physical speed. The analytic single-frame event is at `t=0`.
|
not a local physical speed. The single-frame event defaults to `t=0`; use
|
||||||
|
`--observer-time T` to select another coordinate time (any finite value,
|
||||||
|
including negative times). Metric evaluation and past-directed ray tracing
|
||||||
|
start at that event, and saved lens maps retain its coordinate time.
|
||||||
|
|
||||||
| Option | Meaning / default |
|
| Option | Meaning / default |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| `--observer-time T` | Single-frame camera coordinate time, default `0`; cannot be combined with movie/track or lens-map input |
|
||||||
| `--observer-position X Y Z` | Coordinate position; if look is omitted, point toward the origin |
|
| `--observer-position X Y Z` | Coordinate position; if look is omitted, point toward the origin |
|
||||||
| `--look-ra-deg RA`, `--look-dec-deg DEC` | Coordinate look direction; missing angle defaults to RA=90°, Dec=-90° |
|
| `--look-ra-deg RA`, `--look-dec-deg DEC` | Coordinate look direction; missing angle defaults to RA=90°, Dec=-90° |
|
||||||
| `--observer-radius R` | Positive radius used only to infer position, default 30; conflicts with explicit position |
|
| `--observer-radius R` | Positive radius used only to infer position, default 30; conflicts with explicit position |
|
||||||
@@ -618,6 +622,13 @@ overlay alpha-composites image-plane triangle edges as one-pixel-wide 0.5
|
|||||||
linear-gray diagnostic lines at 0.5 opacity. The line rasterizer uses
|
linear-gray diagnostic lines at 0.5 opacity. The line rasterizer uses
|
||||||
coverage-based antialiasing.
|
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
|
## HDR output
|
||||||
|
|
||||||
To preserve a single-frame render for later exposure and tone-mapping work,
|
To preserve a single-frame render for later exposure and tone-mapping work,
|
||||||
|
|||||||
Reference in new issue
Block a user