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)
|
||||
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)
|
||||
$(TONE_MAP_TEST_TARGET)
|
||||
|
||||
@@ -381,7 +381,9 @@ typedef struct {
|
||||
|
||||
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
|
||||
由事件、坐标速度、指向与 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` 独立。
|
||||
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
|
||||
@@ -777,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 协议
|
||||
|
||||
+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)
|
||||
++count;
|
||||
if (count == 0) {
|
||||
fprintf(stderr, "Dummy PSF %s chunks: none\n", label);
|
||||
fprintf(stdout, "Dummy PSF %s chunks: none\n", label);
|
||||
return;
|
||||
}
|
||||
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"};
|
||||
for (size_t field = 0; field < 6; ++field) {
|
||||
qsort(series[field], count, sizeof **series, compare_double);
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
||||
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
||||
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.99), series[field][count - 1]);
|
||||
}
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Dummy PSF %s adaptive criterion: %zu/%zu chunks tile16-eligible "
|
||||
"(events>=8192 and events/occupied_32px_tiles>=32)\n",
|
||||
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) {
|
||||
double *series = values + field * count;
|
||||
qsort(series, count, sizeof *series, compare_double);
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Dummy PSF %s %s: min=%.3f p10=%.3f p25=%.3f p50=%.3f "
|
||||
"p75=%.3f p90=%.3f p99=%.3f max=%.3f\n",
|
||||
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),
|
||||
series[count - 1]);
|
||||
}
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Dummy PSF %s support totals: references=%zu tasks=%zu summed_first_pass=%.3f s\n",
|
||||
label, total_refs, total_tasks, total_pass);
|
||||
free(values);
|
||||
@@ -407,7 +407,7 @@ void dummy_psf_sink_report(const DummyPsfSink *sink) {
|
||||
size_t full = 0;
|
||||
for (size_t i = 0; i < sink->count; ++i)
|
||||
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 chunks: total=%zu full=%zu partial=%zu events=%zu "
|
||||
"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, 1, "full");
|
||||
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, 1, "full");
|
||||
}
|
||||
|
||||
+4
-4
@@ -2260,7 +2260,7 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh,
|
||||
(size_t)dummy_workers, completed, 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_workers, omp_get_wtime() - dummy_start);
|
||||
copy_psf_splat_stats(psf_stats, (CatalogSplatStats){
|
||||
@@ -2349,14 +2349,14 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh,
|
||||
}
|
||||
omp_destroy_lock(&submit_lock);
|
||||
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);
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"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.selection_seconds, owner.hip_timing.bin_seconds);
|
||||
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",
|
||||
owner.hip_timing.tile_reference_count, owner.hip_timing.tile_task_count,
|
||||
owner.hip_timing.tile_merge_count,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -605,7 +605,7 @@ static int submit_prepared(HipPsfSink *sink, HipPsfPreparedChunk *prepared,
|
||||
sink->timing.event_count += event_count;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
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",
|
||||
sink->timing.event_count, sink->completed_events,
|
||||
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;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
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.batch_count, sink->timing.kernel_seconds);
|
||||
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,
|
||||
job->height, settings))
|
||||
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);
|
||||
if (job->draw_mesh && job->mesh_rgb8 != NULL) {
|
||||
if (write_rgb8_image(job->mesh_path, job->mesh_rgb8, job->width,
|
||||
job->height, settings))
|
||||
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->fast_mode)
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Fast PSF splats: deposited %zu, wing-clipped %zu, discarded "
|
||||
"below min-Y %zu\n",
|
||||
job->psf_stats.cached_splats, job->psf_stats.cached_wing_clipped,
|
||||
job->psf_stats.discarded_below_min_y);
|
||||
else
|
||||
psf_kernel_cache_report(NULL, &job->psf_stats, stderr);
|
||||
psf_kernel_cache_report(NULL, &job->psf_stats, stdout);
|
||||
}
|
||||
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_download_seconds);
|
||||
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
|
||||
if (stats != NULL)
|
||||
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_frame_seconds = omp_get_wtime() - start;
|
||||
if (accumulator->verbose)
|
||||
fprintf(stderr,
|
||||
fprintf(stdout,
|
||||
"Fast FFTW frame: zero_pack=%.6f forward=%.6f "
|
||||
"multiply=%.6f inverse=%.6f crop_downsample=%.6f "
|
||||
"total=%.6f\n",
|
||||
|
||||
@@ -130,9 +130,9 @@ def endpoint_deviation(a, b):
|
||||
return mismatches, worst
|
||||
|
||||
|
||||
def trace_cost(stderr, label):
|
||||
def trace_cost(text, label):
|
||||
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()
|
||||
|
||||
|
||||
@@ -263,8 +263,8 @@ with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
|
||||
replay_hdr = replay_out.with_name(replay_out.stem + '_HDR.fits')
|
||||
assert dflt_out.with_name(dflt_out.stem + '_HDR.fits').read_bytes() \
|
||||
== replay_hdr.read_bytes()
|
||||
live_cost = trace_cost(dflt_run.stderr, 'Frame 0')
|
||||
replay_cost = trace_cost(replay_run.stderr, 'Imported map')
|
||||
live_cost = trace_cost(dflt_run.stdout, 'Frame 0')
|
||||
replay_cost = trace_cost(replay_run.stdout, 'Imported map')
|
||||
assert live_cost is not None and replay_cost is not None
|
||||
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]
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-', dir='/tmp/opencode') as directory:
|
||||
tmp = Path(directory)
|
||||
for backend in ('minkowski', 'schwarzschild'):
|
||||
binary = BUILD / f'{backend}_sky'
|
||||
help_text = run(binary, '--help').stdout
|
||||
ext = 'png' if '.png' in help_text else 'ppm'
|
||||
hdr_available = '--hdr-output' in help_text
|
||||
for option in ('--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
||||
hdr_available = any(line.startswith(' --hdr-output ')
|
||||
for line in help_text.splitlines())
|
||||
for option in ('--observer-time', '--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
||||
assert option in help_text
|
||||
assert '--tone-map' in help_text and '--tone-map-p' 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 = run(binary, *common, '--fast-mode', '--fast-supersample', 2,
|
||||
'--output', fast_path)
|
||||
assert 'Fast FFTW:' in fast.stderr, fast.stderr
|
||||
assert 'Fast FFTW:' in fast.stdout, fast.stdout
|
||||
assert image_payload(fast_path)
|
||||
|
||||
# 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,
|
||||
'--look-ra-deg', 0, '--look-dec-deg', -90)
|
||||
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)
|
||||
assert default == render('default_explicit', '--observer-position', *pos,
|
||||
'--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,
|
||||
ok=False)
|
||||
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 'PSF cache ready' not in 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.stdout + too_long.stderr)
|
||||
|
||||
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, 'nan'], None),
|
||||
(['--observer-velocity', 0, 0, 'inf'], None),
|
||||
@@ -275,7 +294,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
if message:
|
||||
assert message in result.stderr, 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'
|
||||
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,
|
||||
'--output', bloom_output)
|
||||
assert image_payload(bloom_output) != baseline
|
||||
assert 'Sensor bloom:' in bloom_run.stderr, bloom_run.stderr
|
||||
report = bloom_run.stderr.split('Sensor bloom:', 1)[1].splitlines()[0]
|
||||
assert 'Sensor bloom:' in bloom_run.stdout, bloom_run.stdout
|
||||
report = bloom_run.stdout.split('Sensor bloom:', 1)[1].splitlines()[0]
|
||||
fields = dict(token.split('=', 1) for token in report.split() if '=' in token)
|
||||
assert int(fields['saturated']) > 0, 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,
|
||||
'--frames-prefix', 'mixed', '--verbose',
|
||||
'--lens-map-output', parallel_map)
|
||||
assert 'Ray trace generation 1: frame 0 added' in result.stderr
|
||||
assert 'Ray trace generation 0: frame 1 added' not 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.stdout
|
||||
for frame in range(2):
|
||||
image_payload(tmp / f'mixed_{frame:06d}.{ext}',
|
||||
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'
|
||||
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)
|
||||
|
||||
# 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 <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});
|
||||
|
||||
@@ -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
|
||||
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 |
|
||||
| --- | --- |
|
||||
| `--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 |
|
||||
| `--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 |
|
||||
@@ -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
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user